Elixir Protocols vs Clojure Multimethods

I am coming to appreciate José Valim’s creation, Elixir, very much. It is fair to say that Rich Hickey set a very high bar with Clojure and Elixir for the most part saunters over it. I think that is a reflection of two very thoughtful language designers at work. But there is some chafing when moving from Clojure to Elixir.

I wrote previously about how Clojure’s metadata feature lets one subtly piggyback data in a way that doesn’t require intermediate code to deal with it, or even know it exists. It creates a ‘functional’ backchannel. Elixir has no equivalent feature.

If your data is a map you can use “special” keys for your metadata. Elixir does this itself with the __struct__ key that it injects into the maps it uses as the implementation of structs. You mostly don’t know it’s there but would have to implement a special case if you ever treated the struct as a map.

However, if the value you want to attach metadata to is, say, an anonymous function then you’re out of luck. In that case, you have to convert your function to a map containing the function and metadata and then change your entire implementation. That could be a chore, or worse.

Today I hit the problem of wanting to define a function with multiple implementations depending on its arguments. Within a module, this is not hard to do as Elixir functions allow for multiple heads using pattern matching. It’s one of the beautiful things about writing Elixir functions. So:

defmodule NextWords do
  def next-word("hello"), do: "world"
  def next-word("foo"), do: "bar"
end

Works exactly as you would expect. Of course, the patterns can be considerably more complex than this and allow you to, for example, match values inside maps. So you could write:

def doodah(%{key: "foo"}), do: "dah"
def doodah(%{key: "dah"}), do: "doo"

And that would work just as you expect too! Fantastic!

But what about if you want the definitions of the function spread across different modules?

defmodule NextWords1 do
  def next-word("hello"), do: "world"
end

defmodule NextWords2 do
  def next-word("foo"), do: "bar"
end

This does not work because while NextWords1.next-word and NextWords2.next-word share the same name they are in all other respects unrelated functions. In Elixir a function is specified as a tuple of {module, name, arity} so functions in different modules are totally separate regardless of name & arity.

In Clojure, when I need to do something like this I would reach for a multi. A Clojure multi uses a function defined over its arguments to determine which implementation of the multi to use.

(defmulti foo [x y z] (fn [x y z] … turn x, y, z into a despatch value, e.g. :bar-1, :bar-2 or what have you))

(ns 'bar-1)
(defmethod foo :bar-1 [x y z] … implementation)

(ns 'bar-2)
(defmethod foo :bar-2 [x y z] … another implementation)

The dispatch function returns a dispatch value and the methods are parameterised on dispatch value. Different method implementations can live in different namespaces and a call with the right arguments will always resolve to the right implementation, regardless of where it was defined.

Now Elixir has an equivalent to multi, the Protocol. We can define the foo protocol:

defprotocol Fooish do
  def foo(x)
end

Now in any module, we can define an implementation for our Fooish protocol. But, and here’s the rub, an implementation looks like:

defmodule Foo-1 do
  defimpl Fooish, for: String
    def foo(s), do: …impl…
  end
end

So an Elixir protocol can only be parameterised on the type of its first argument! This means that there’s no way to dispatch protocol implementations based on lovely pattern matching. Disappointing.

It may even be that a smarter Elixir programmer than I could implement Clojure style multi-methods in Elixir. For now, I can find a work-around and I’m still digging Elixir a lot.

One thought on “Elixir Protocols vs Clojure Multimethods”

  1. Elixir Forum is generally the best place to find savvy and helpful Alchemists.

Leave a Reply to Rich Morin Cancel reply

Your email address will not be published. Required fields are marked *