While building and analyzing code in Elixir, you have probably come across the keywords behavior
, impl
, defprotocol
and defimpl
, while searching you may have found a short description that can confuse you about when to use them and their differences, this post brings some examples of when to use them and how to implement them in a real project.
Behaviour
Behaviour is a way to implement a user interface for a module to share its api in a public way, this module must have functions that need to be used in the ones that implement this behaviour.
Let's get to the examples:
Suppose we need to define an interface for our parsers, for this we will create a module:
defmodule Parser do
@callback parse_ids(map) :: map
@spec to_downcase(map) :: map
def to_downcase(attrs) do
attrs
|> Map.new(fn {i, v} -> {i, String.downcase(v)} end)
end
end
In this case we create a Parser module with an @callback
and a function that takes a map and transforms its values into downcase.
@callback
is the function that should be implemented by all modules that use the Parser behavior.
Let's look at an example of its use.
defmodule Person do
@behaviour Parser
@impl Parser
def parse_ids(attrs) do
attrs
|> Map.put_new("name", get_in(attrs, ["user", "name"]))
|> Map.put_new("email", get_in(attrs, ["user", "email"]))
|> Parser.to_downcase()
end
end
Here we use the Parser
behaviour to define the parse_ids
function, otherwise the compiler will return a warning.
Although not required, the @impl
ensures that we are implementing the correct callbacks.
Behaviours are a great way to ensure behavior when we need to export a module's public api, as they have types defined in their construction.
Protocols
In a very direct way, protocols are a way to implement polymorphism in Elixir, we use them when we need a module to have a different behavior depending on the type of the value.
Here are some examples:
defprotocol Document do
@spec id(any) :: any
def id(item)
@spec encode(any) :: map
def encode(item)
end
Here we define our protocol, so everyone who uses it can create the function with a return that is within the specifications and is for a unique type.
Let's use the protocol.
defmodule Person do
embedded_schema do
field :name, :string
field :social_security_number, :string
end
defimpl Document, for: __MODULE__ do
def id(struct), do: struct.social_security_number
def encode(struct) do
struct
|> Person.to_map()
end
end
def to_map(struct) do
%{
name: struct.name,
social_security_number: struct.social_security_number
}
end
end
Here we define the use of the Document protocol with defimpl, see that we are defining the type, through for: __MODULE__
, that way when Document.encode is called, it already knows it will return a map, the definition of which map it will return is in the module specification, very separate responsibilities.
But what is the difference between Behaviour and Protocol?
A good example is given by the creator of the language, José Valim, in this post: https://groups.google.com/forum/#!msg/elixir-lang-talk/S0NlOoc4ThM/J2aD2hKrtuoJ
"""A protocol is indeed a behaviour + dispatching logic.
However I think you are missing the point of behaviours. Behaviours are extremely useful. For example, a GenServer defines a behaviour. A behaviour is a way to say: give me a module as argument and I will invoke the following callbacks on it, which these argument and so on. A more complex example for behaviours besides a GenServer are the Ecto adapters.
However, this does not work if you have a data structure and you want to dispatch based on the data structure. Hence protocols."""
I appreciate everyone who has read through here, if you guys have anything to add, please leave a comment.
Top comments (0)