Understanding GenServer Function Clause Matching

Hello! Going through Sasa Juric’s Elixir in Action. In 6.2.8 we’re implementing a GenServer. Please ignore the two modules in one :wink:

defmodule TodoServer do
  use GenServer

  def start do
    GenServer.start(TodoServer, nil)
  end

  def add_entry(todo_server, new_entry) do
    GenServer.cast(todo_server, {:add_entry, new_entry})
  end

  def entries(todo_server, date) do
    GenServer.call(todo_server, {:entries, date})
  end

  @impl GenServer
  def init(_) do
    {:ok, TodoList.new()}
  end

  @impl GenServer
  def handle_cast({:add_entry, new_entry}, todo_list) do
    new_state = TodoList.add_entry(todo_list, new_entry)
    {:noreply, new_state}
  end

  @impl GenServer
  def handle_call({:entries, date}, _, todo_list) do
    {
      :reply,
      TodoList.entries(todo_list, date),
      todo_list
    }
  end
end

defmodule TodoList do
  defstruct auto_id: 1, entries: %{}

  def new(entries \\ []) do
    Enum.reduce(
      entries,
      %TodoList{},
      &add_entry(&2, &1)
    )
  end

  def add_entry(todo_list, entry) do
    entry = Map.put(entry, :id, todo_list.auto_id)
    new_entries = Map.put(todo_list.entries, todo_list.auto_id, entry)

    %TodoList{todo_list | entries: new_entries, auto_id: todo_list.auto_id + 1}
  end

  def entries(todo_list, date) do
    todo_list.entries
    |> Stream.filter(fn {_, entry} -> entry.date == date end)
    |> Enum.map(fn {_, entry} -> entry end)
  end

  def update_entry(todo_list, %{} = new_entry) do
    update_entry(todo_list, new_entry.id, fn _ -> new_entry end)
  end

  def update_entry(todo_list, entry_id, updater_fun) do
    case Map.fetch(todo_list.entries, entry_id) do
      :error ->
        todo_list

      {:ok, old_entry} ->
        new_entry = updater_fun.(old_entry)
        new_entries = Map.put(todo_list.entries, new_entry.id, new_entry)
        %TodoList{todo_list | entries: new_entries}
    end
  end

  def delete_entry(todo_list, entry_id) do
    %TodoList{todo_list | entries: Map.delete(todo_list.entries, entry_id)}
  end
end

In iex, I’ll try-

iex(1)> todo_server = TodoServer.start()
{:ok, #PID<0.116.0>}
iex(2)> TodoServer.add_entry(todo_server, %{date: ~D[2018-12-20], title: "Shopping"})

To then receive a match error-

** (FunctionClauseError) no function clause matching in GenServer.cast/2

    The following arguments were given to GenServer.cast/2:

        # 1
        {:ok, #PID<0.116.0>}

        # 2
        {:add_entry, %{date: ~D[2018-12-20], title: "Shopping"}}

    Attempted function clauses (showing 4 out of 4):

        def cast({:global, name}, request)
        def cast({:via, mod, name}, request)
        def cast({name, node}, request) when is_atom(name) and is_atom(node)
        def cast(dest, request) when is_atom(dest) or is_pid(dest)

    (elixir 1.12.0) lib/gen_server.ex:1045: GenServer.cast/2

I understand I’m not giving correct function clause parameters to match with GenServer.cast/2, just can’t quite tell from the context or the docs what I should be feeding it!

Thanks for any help :slight_smile:

cast etc expect either a PID or a name; you’re passing a tuple of {:ok, pid}.

A very common pattern in Elixir code is “unwrapping” :oks:

iex(1)> {:ok, todo_server} = TodoServer.start()
{:ok, #PID<0.116.0>}

iex(2)> TodoServer.add_entry(todo_server, %{date: ~D[2018-12-20], title: "Shopping"})
...should work now...

Here the first line matches the tuple from TodoServer.start but only binds the PID part to a variable (todo_server).

2 Likes

Great! Works indeed. Thanks! I need to do some further reading to fully understand this concept.

Unwrapping the :ok is new to me.

It helps to remember that = in Elixir is not an assignment as it is in many other languages, but is a match. So if you write {:ok, hmm} = some_call(), you can think of it sort of like making an assertion on the result of some_call(). It’s very possible that the call returns something other than {:ok, value} (conventionally, {:error, error}), and in that case, a MatchError would be raised.

If you want to handle the error, you could use a case, like:

case some_call() do
  {:ok, result} -> :happy_case
  {:error, error} -> :sad_case
end

case is just a way to try multiple matches, whereas = gives you only “one shot” at matching. Of course, in a REPL, it’s much easier to just assume the happy case.

1 Like