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 naï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.