Elixir Protocols

Protocols are a powerful language feature in Elixir. It allows you to specify an api which should be defined by its implementation. If you ever need to create functions that accept polymorphic types, protocols allow you to do so in a managed and organized manner.

Although Elixir Behaviours are similar, Behaviours are defined internally within each module. Protocol implementations can occur externally outside of the module. This allows for extending a module’s functionality you don’t have access to.

Suppose we created an Odd protocol which returns true or false depending on the type.

1 defprotocol Odd do
2   @doc "Returns true if the data is considered odd"
3   @fallback_to_any true
4 
5   def odd?(data)
6 end

@doc describes what the protocol does. The function to be implemented is def odd?(data). @fallback_to_any is an attribute which specifies that a default implementation of Any is to be used in the event that no specific implementation of Odd is found for that particular type.

To implement the protocol:

 1 defimpl Odd, for: Integer do
 2   require Integer
 3 
 4   def odd?(num) do
 5     Integer.is_odd(num)
 6   end
 7 end
 8 
 9 defimpl Odd, for: Float do
10   def odd?(num) do
11     Odd.odd?(trunc(num))
12   end
13 end
14 
15 defimpl Odd, for: List do
16   def odd?(data) do
17     Odd.odd?(Enum.count(data))
18   end
19 end
20 
21 defimpl Odd, for: Any do
22   def odd?(_), do: false
23 end

defimpl defines the Odd protocol for the type specified in for in this case for both Integer and Float types.

We define an integer as odd if Integer.is_odd macro returns true although we can also write our own implementation here.

For a float, we define it as odd if the integer part of the float is odd. Since trunc returns an integer, we can pass it to the earlier implementation of odd for Integer through Odd.odd?

For lists, it is odd if it has an odd number of elements.

The final implementation is a default fallback for any data type for which there is no odd protocol implementation. This is required by the @fallback_to_any attribute else it will not compile.

Now we can call it like so:

1 Odd.odd?(1) # true
2 
3 Odd.odd?(2) # false
4 
5 Odd.odd?(1.9) # true
6 
7 Odd.odd?(2.1) # false
8 
9 Odd.odd?([1]) # true

To test the implementation directly:

1 Odd.Integer.odd?(1) # true
2 
3 Odd.Float.odd?(1.9) # true

For data types which don’t implement Odd it will automatically trigger the Any implementation of protocol which always returns false:

1 Odd.odd?(%{}) # false
2 
3 Odd.odd?(:atom) # false
4 
5 Odd.impl_for(%{}) # Odd.Any

We can also define protocols for user defined data types such as structs.

 1 # assuming we created an Animal struct
 2 
 3 defmodule Animal do
 4   defstruct [:hairy]
 5 end
 6 
 7 defimpl Odd, for: Animal do
 8   def odd?(%Animal{hairy: true}), do: true
 9   def odd?(_), do: false
10 end

Here, we define an Animal to be odd if it is hairy.

1 Odd.odd?(%Animal{hairy: true}) # true
2 
3 Odd.odd?(%Animal{hairy: false}) # false

There are 2 useful introspection functions for protocols which I find useful:

  • __protocol__(:functions)

    List all functions and their arity as defined by the protocol

  • impl_for(structure)

    Returns module which implements protocol for that structure.

Example usage:

1 Odd.__protocol__(:functions) #=> [:odd]
2 
3 Odd.impl_for(%Animal{}) #=> Odd.Animal
4 
5 Odd.impl_for(1) #=> Odd.Integer

Redefining an existing module behaviour

Lets assume we want to redefine the print format of Animal struct. inspect calls Inspect.inspect and Inspect is a protocol. We can redefine it like so:

 1 defimpl Inspect, for: Animal do
 2   def inspect(animal, _opts) do
 3     # convert animal struct to map to get the attributes:
 4     attr_str = Map.delete(animal, :__struct__)
 5                |> Enum.reduce("", fn({k,v}, acc) -> acc <> "* #{k} -> #{v}" end)
 6 
 7     """
 8     Animal has the following attrs:
 9 
10     #{attr_str}
11     """
12   end
13 end

Now when we call inspect on an Animal struct, we get the following:

1 IO.inspect %Animal{hairy: false}
2 
3 # => Animal has the following attrs:
4 # * hairy -> false
5 
6 # to ensure IO.inspect is calling Inspect.Animal
7 Inspect.impl_for(%Animal{}) # => Inspect.Animal

Summary

Protocols are a powerful feature to help support polymorphism in Elixir. We have seen how to define our custom protocol for standard system types as well as for our own structs. We have also seen a trivial example of how to redefine a system built protocol for our own ends.

A sample github repository for this post is available.

Keep hacking and stay curious!!

Further information