Concise way of writing multi clause - single body functions?

While writing a GenServer implementation that keeps record of a large number of event, I realized that I am writing lots of boilerplate code and started wondering if there is a better syntax for writing functions with multiple clauses.

Here is my code (truncated for readability)

defmodule EventCounter do
  use GenServer

  # usage
  # > {:ok, h} = EventCounter.start_link()
  # > GenServer.call(h, :ev20)
  # :ok
  # > GenServer.call(h, :ev21)
  # {:error, "invalid event"}
  # > GenServer.call(h, {:get_count, :ev20})
  # 1

  @impl true
  def handle_call({:get_count, event}, _from, state),
    do: {:reply, Map.get(state, event, 0), state}

  # handlers for all valid events
  @impl true
  def handle_call(:ev01 = valid_event, _from, state), do: {:reply, :ok, count_events(state, valid_event)}
  @impl true
  def handle_call(:ev02 = valid_event, _from, state), do: {:reply, :ok, count_events(state, valid_event)}
  @impl true
  def handle_call(:ev03 = valid_event, _from, state), do: {:reply, :ok, count_events(state, valid_event)}
  # --- HERE COME HANDLERS FOR EV04 TO EV19 ---
  @impl true
  def handle_call(:ev20 = valid_event, _from, state), do: {:reply, :ok, count_events(state, valid_event)}

  # fallback handler
  @impl true
  def handle_call(_illegal_event, _from, state), do: {:reply, {:error, "invalid event"}, state}

  @impl true
  def init(state), do: {:ok, state}

  def start_link() do
    GenServer.start_link(__MODULE__, %{})
  end

  defp count_events(state, event), do: state |> Map.update(event, 1, &(&1 + 1))
end

As you can see I am having 20 handle_call’s having the same implementation.

I was hoping that I could make it tidy by using function headers, something like this:

# handlers for all valid events
  @impl true
  def handle_call(valid_event, _from, state)
  def handle_call(:ev01, _from, state)
  def handle_call(:ev02, _from, state)
  def handle_call(:ev03, _from, state)
  # --- HERE COME HANDLERS FOR EV04 TO EV19 ---
  def handle_call(:ev20, _from, state),
    do: {:reply, :ok, count_events(state, valid_event)}

but this didn’t work.

I believe that this is a common problem. Does anyone know a way to avoid the boilerplate code while allowing only the valid events?

What do you expect this code to do? If you want to bind the event-atom to valid_event (if its one of those used) and then fall through to the function with the do-block, you could do:

def handle_call(valid_event, _from, state) when valid_event in [:ev01, ...] do ...
3 Likes

This guard solves my problem. I did not know that the “in” operator exists.

In my real life use case I am composing my final EventHandler out of multiple modules (included via “use”) which handle specific types of events, and I needed a way to dispatch events at the level of functions. This guard solves my problem. Thank you.

As an alternative to the excellent suggestion of @Sebb, you can use macros to code-generate one function per event.

As a disclaimer: I do that in library projects but never in application projects – there readability for the work colleagues is always top priority so I rarely (if ever) reach for macros in my code because that’s often reviewed by other people in PRs.

3 Likes

see this thread: Can a macro generate functions iteratively from list of atoms?

Note, that it does not just exist, but is also allowed in guards (as long as the right-hand side is a list or a range).

1 Like

@dimitarvp Thanks for bringing this up. Actually with an accumulating attribute I could make it much clearer.

Note, that it does not just exist, but is also allowed in guards (as long as the right-hand side is a list or a range).

@Sebb and this is what surprised me. I always remembered guards as simple type checkers. Apparently the language evolved since I used it last time.

On a side note, I realized that this guard-based solution is perfect when all functions match up in terms of general pattern (which includes my case, which makes me happy), but in case each event would come with extra data of variable size and structure, the syntax would bloat again.

So I am thinking that it still would be nice to have a pattern-based solution like:

@impl true
def handle_call({:ev01 = valid_event, [_|_] = arg0}, _from, state)
or
def handle_call({:ev02 = valid_event, %{} = arg0, arg1}, _from, state) when is_atom(arg1)
or
def handle_call({:ev03 = valid_event, arg0, arg1}, _from, state) when is_atom(arg0) and is_integer(arg1) do
  {:reply, :ok, count_events(state, valid_event)}
end

(that was the idea for the pseudo-code in the original question)

Your latest code can also be generated by a for comprehension with unquote fragments and a slightly more complex data structure used as a source material for generating the code – including the varying length of the function argument lists (by using unquote_splicing).

As mentioned before, you just have to decide for yourself if your main priority is immediate readability for future devs, or is it the code brevity and the declarative approach that the meta-programming gives you.

1 Like

the reduction to (compile time) lists and ranges makes it simple:

when x in [1, 2, 3]

translates to:

when x === 1 or x === 2 or x === 3

see: Kernel — Elixir v1.12.3

EDIT:

I just wonder whats going on in guards with step-ranges, this can’t be true:

when x in 1..3

translates to:

when is_integer(x) and x >= 1 and x <= 3

for a range with step.

1 Like