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
2
3
4
5
6
defprotocol Odd do
@doc "Returns true if the data is considered odd"
@fallback_to_any true
def odd?(data)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
defimpl Odd, for: Integer do
require Integer
def odd?(num) do
Integer.is_odd(num)
end
end
defimpl Odd, for: Float do
def odd?(num) do
Odd.odd?(trunc(num))
end
end
defimpl Odd, for: List do
def odd?(data) do
Odd.odd?(Enum.count(data))
end
end
defimpl Odd, for: Any do
def odd?(_), do: false
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
2
3
4
5
6
7
8
9
Odd.odd?(1) # true
Odd.odd?(2) # false
Odd.odd?(1.9) # true
Odd.odd?(2.1) # false
Odd.odd?([1]) # true
To test the implementation directly:
1
2
3
Odd.Integer.odd?(1) # true
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
2
3
4
5
Odd.odd?(%{}) # false
Odd.odd?(:atom) # false
Odd.impl_for(%{}) # Odd.Any
We can also define protocols for user defined data types such as structs.
1
2
3
4
5
6
7
8
9
10
# assuming we created an Animal struct
defmodule Animal do
defstruct [:hairy]
end
defimpl Odd, for: Animal do
def odd?(%Animal{hairy: true}), do: true
def odd?(_), do: false
end
Here, we define an Animal to be odd if it is hairy.
1
2
3
Odd.odd?(%Animal{hairy: true}) # true
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
2
3
4
5
Odd.__protocol__(:functions) #=> [:odd]
Odd.impl_for(%Animal{}) #=> Odd.Animal
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
2
3
4
5
6
7
8
9
10
11
12
13
defimpl Inspect, for: Animal do
def inspect(animal, _opts) do
# convert animal struct to map to get the attributes:
attr_str = Map.delete(animal, :__struct__)
|> Enum.reduce("", fn({k,v}, acc) -> acc <> "* #{k} -> #{v}" end)
"""
Animal has the following attrs:
#{attr_str}
"""
end
end
Now when we call inspect
on an Animal struct, we get the following:
1
2
3
4
5
6
7
IO.inspect %Animal{hairy: false}
# => Animal has the following attrs:
# * hairy -> false
# to ensure IO.inspect is calling Inspect.Animal
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!!