Derive Them Protocols

January 6, 2019

Using derive to implement a protocol is a quick way to give functionality to your modules.

Let’s say you had a protocol that rounds values for you. Like such:

defprotocol Math do
  def round(v, p)
end

defimpl Math, for: Float do
  defdelegate round(v, p), to: Float
end

Which allows us to do something like this:

iex(1)> Math.round(1.556, 2)  
1.56

Now say we wanted to round all the float values in our custom data structures. Instead of having to implement the logic over and over would be nice if we could do it simply once.

Here is an example data structure.

defmodule Data do
  defstruct x: 1.556, y: 2.556
end

If we tried to use our protocol as it stands now on the Data structure we’d get:

iex(1)> Math.round(%Data{}, 2)
** (Protocol.UndefinedError) protocol Math not implemented for %Data{x: 1.556, y: 2.556}. This protocol is implemented for: Float
    (super_happy_fun_land) lib/math.ex:1: Math.impl_for!/1
    (super_happy_fun_land) lib/math.ex:2: Math.round/2

Let’s implement the Math protocol for any such data structure in our system to take advantage of.

defimpl Math, for: Any do
  defmacro __deriving__(module, _struct, _opts) do
    quote do
      defimpl Math, for: unquote(module) do
        def round(data, p) do
          rounded_map =
            data
            |> Map.from_struct()
            |> Enum.reduce(%{}, fn {k, v}, acc ->
              Map.merge(acc, %{k => round_float(v, p)})
            end)

          struct(unquote(module), rounded_map)
        end

        defp round_float(v, p) when is_float(v), do: Math.round(v, p)
        defp round_float(v, _), do: v
      end
    end
  end
end

We’ve implemented the Math protocol for Any and then defined the deriving macro which will allow us to use this in our modules.

Then we can implement the round function for any module that takes advantage of the derive.

Finally let’s add this to our Data module and give it a try.

defmodule Data do
  @derive [Math]
  defstruct x: 1.556, y: 2.556
end

iex(1)> Math.round(%Data{}, 2)
%Data{x: 1.56, y: 2.56}

As you can see using derive is a nice tool if you have lots of data structures that all need to implement a protocol in a simple fashion and saves you the time from having to repeat yourself over and over.


Contact Me -> Click!