Protocols and Behaviours in Elixir

Protocols and Behaviours in Elixir are similar to interfaces in languages such as C#, Java etc.

Protocols can be thought of as interfaces for data whereas behaviours are like interfaces for modules, let’s see what this really means…

Protocols

A protocol is available for a data type, so let’s assuming we want a toString function on several data types but we obviously cannot cover all possible types that may be created in the future, i.e a Person struct or the likes. We can define a protocol which can be applied to data types, like this…

Let’s start by define the protocol

defprotocol Utils do
  @spec toString(t) ::String.t()
  def toString(value)
end

Basically we’re declaring the specification for the protocol using the @spec annotation. This defines the inputs and outputs, taking any params the after the :: is the return type. Next we define the function.

At this point we have now implementations, so let’s create a couple of implementations for a couple of the standard types, String and Integer

defimpl Utils, for: String  do
  def toString(value), do: "String: #{value}"
end

defimpl Utils, for: Integer  do
  def toString(value), do: "Integer: #{value}"
end

The for is followed by the data type supported by this implementation. So as you can see, we have a couple of simple implementation, but where protocols become more important is that we can now define the toString function on other types, let’s assume we have the Person struct from a previous post

defmodule Person do
  @enforce_keys [:firstName, :lastName]
  defstruct [:age, :firstName, :lastName]

  def create() do
    %Person{ firstName: "Scooby", lastName: "Doo", age: 30 }
  end
end

and we want to give it a toString function, we would simply define a new implementation of the protocol for the Person data type, like this

defimpl Utils, for: Person  do
  def toString(value), do: "Person: #{value.firstName} #{value.lastName}"
end

Now from iex or your code you can do sometihing like this

scooby = Parson.create()
Utils.toString(scooby)

and you’ve got toString working with the Person type.

Behaviours

Behaviours are again similar to interfaces but are used to define what a module is expected to implement. Let’s stick with the idea of a toString function which just outputs some information about the module that’s implementing it, but this time we’re expecting a module to implement this function, so we declare the behaviour as follows

defmodule UtilBehaviour do
  @callback toString() :: String.t()
end

We use the @callback annotation to declare the expected function(s) and @macrocallback for macros. As per the protocol we give the signature of the function followed by :: and the expected return type.

Now to implement this, let’s again go to our Person struct (remember this version of toString is just going to output some predefined string that represents the module)

defmodule Person do
  @behaviour UtilBehaviour

  @enforce_keys [:firstName, :lastName]
  defstruct [:age, :firstName, :lastName]

  def create() do
    %Person{ firstName: "Scooby", lastName: "Doo", age: 30 }
  end

  def toString() do
    "This is a Person module/struct"
  end
end

Now our module implements the behaviour and using Person.toString() outputs “This is a Person module/struct”.

We can also use the @impl annotation to ensure that you explicitly define the behaviour being implement like this

@impl UtilBehaviour
def toString() do
  "This is a Person module/struct"
end

This @impl annotation tells the compiler explicitly what you’re implementing, this is just an aid to development by making it clear what’s implementing what. If you use @impl once you have to use it on every behaviour.