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.