Pattern matcher for Protocols

· 3 min read

elixir tricks dwim hacking

One of the best features of Erlang (and hence Elixir) is the ability to pattern match and use guards directly in function clauses:

defmodule M do
  def m(nil), do: "nil given"
  def m([]), do: "empty list given"
  def m([h|_]), do: "non-empty list given (head: #{inspect(head)}.)"
  def m(msg) when is_binary(message), do: "message given (msg: #{msg}.)"
end

That works perfectly, routing the calls to respective handlers. But what if we want to pattern match the implementation of the protocol ?

That is not possible out of the box and might seem to be tricky. The naiฬˆve approach would be to emulate pattern matching:

defprotocol P, do: def pm(this), do: inspect(this)

defmodule M do
  def m(p) do
    unless P.impl_for(p), do: raise MatchError, term: p
    # ...
  end
end

That kinda works, but hey! We have all the ingredients: we have macros in Elixir, we have Kernel#reflections for protocols, we also have an ardour. Letโ€™s cook the matchers ourselves.

We are going to generate all the clauses for all the consolidated protocols, known to the system. Letโ€™s do it:

defmodule ProtoMatcher do
  defmacro defprotomatchers(name, mod, fun) do
    quote do
      with {:consolidated, mods} <- unquote(mod).__protocol__(:impls) do
        for impl <- mods do
          def unquote(name)(%{__struct__: impl} = this) do
            unquote(fun).(this)
          end
        end
      else
        :not_consolidated ->
          raise [
            "Protocols are not consolidated.",
            "That usually happens in `iex`.",
            "Please start iex as `iex -S mix`",
            "    in the project directory to use this feature."]
            |> Enum.join("\n")
      end
    end
  end
end

Now this might be used as:

defmodule M do
  require ProtoMatcher
  ProtoMatcher.defprotomatchers(:checker, Enumerable, fn this -> inspect(this) end)
end

M.checker(%{})
#โ‡’ย ** (FunctionClauseError) no function clause matching in M.checker/1

M.checker(%File.Stream{})
#โ‡’ย "%File.Stream{line_or_bytes: :line, modes: [], path: nil, raw: true}"

That said, the defmodule M above produced:

{:module, M,
 <<70, 79, 82, 49, 0, 0, 5, 56, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 174,
   0, 0, 0, 17, 8, 69, 108, 105, 120, 105, 114, 46, 77, 8, 95, 95, 105, 110,
   102, 111, 95, 95, 9, 102, 117, 110, 99, ...>>,
 [
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1,
   checker: 1
 ]}

with 11 implementations of M.checker/1 :

M.__info__(:functions)
#โ‡’ย [checker: 1]

Now you have a function (a set of 11 functions, to be precise,) that effectively matches only implementations of the Enumerable protocol.


There is definitely a room for improvements, like packing everything into a __using__/1 macro, allowing blocks and delegates besides the anonymous function as a handler, better customization for how to handle different implementations, callbacks, cold beer...

Maybe once Iโ€™ll publish this as a package to hex, but for now this blog is fine enough.