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)