I have recently been reading about Behaviours VS Protocols. In a number of articles I read I found that the implementations of behaviours differs quite a lot from what I have read in the book.
@behaviour
The first mention I see of this macro can be found here:
Specifically, the article gives an example from Swoosh:
defmodule Swoosh.Adapters.Local do
@behaviour Swoosh.Adapter
def deliver(%Swoosh.Email{} = email, _config) do
%Swoosh.Email{headers: %{"Message-ID" => id}} = Swoosh.InMemoryMailbox.push(email)
{:ok, %{id: id}}
end
end
Here you can see the use of the macro @behaviour.
use
However, when reading the book Elixir in Action ( 2nd edition ) and when checking the code samples for GenServer, I always find something with the following structure:
defmodule TodoServer do
use GenServer
#... code here
end
Questions
Why am I not using the @bahviour macro here and use use instead?
What are the differences between the two?
When should I use one or the other?
Why do I have to use use with GenServer instead of @behaviour ?
use GenServer actually injects code that contains @behaviour GenServer as well with some boilerplate code (eg. child_spec/1).
The module attribute (not macro) @behaviour does only associate a behaviour to the module, while use injects code at the place it is used, which may or may not add a behaviour to the module.
I am at a point where I understand the difference between Behaviours and Protocols quite well. This question is just specific to Behaviours though, I want to learn how to do them right.
Of course that isn’t recommended practice because @behaviour gives you a direct clue that you are looking at a callback module (with some very specific constraints).
So behaviours with a base implementation in Erlang like gen_server don’t rely on Elixir mechanisms (though conceivably an Elixir wrapper could add it’s own extensions).
My mental model for behaviours revolves around Kernel.apply/3 (or perhaps more accurately :erlang.apply/3) and specifically its argument types:
If use is supported by the behaviour, I think it would be more idiomatic to go with use instead of manually specifying @behaviour.
In addition, from the library author’s point of view, I think that it’s better if a process-oriented behaviour implemented in Elixir supports use, because it can then inject the default implementation of child_spec/1. By opting for such approach, the behaviour also becomes more similar in usage to GenServer and Supervisor, and non-behaviour process abstractions, such as Task and Agent.
The explicit need for @behaviour is IMO mostly required when using Erlang behaviours (e.g. gen_statem), and an occasional Elixir one which doesn’t need to inject any code into the callback module, and so it doesn’t support use (e.g. Calendar).
So far, what I understand, is that behaviors helps us define a “contract like” module based on a common pattern or logic behavior. For example: A company might have documents that represent debts, say a contract or invoice, both have a common behavior of: amount, due date, provider, payments etc.
So I can define a “debt document” behavior and use it in the invoice and contract modules to address specific details of both documents.
Based on my understanding and whats been commented in this thread I am not clear how to implement a behavior without using @behaviour macro and using the “use” macro instead. The “use” macro will inject the code of the behavior module or the callback module?
Let’s start with the basic implementation. Let’s say we have some behaviour:
defmodule SomeBehaviour do
@callback foo(any) :: any
# ...
end
When we’re writing a callback module for that behaviour, we can specify the @behaviour module attribute:
defmodule SomeCallbackModule do
# Indicates that this module implements callback functions for SomeBehaviour
@behaviour SomeBehaviour
@impl SomeBehaviour
def foo(x) do
# ...
end
end
Now let’s go back to the behaviour module and add the support for use:
defmodule SomeBehaviour do
defmacro __using__(_opts) do
quote do
# the code in this block will be injected into the module where `use SomeBehaviour` is invoked
@behaviour SomeBehaviour
end
end
# the rest of the code remains the same
end
Now in the callback module we can replace @behaviour SomeBehaviour with use SomeBehaviour:
defmodule SomeCallbackModule do
use SomeBehaviour
# the rest of the code remains the same
end
So to summarize the example above, in the behaviour module you define the __using__/1 macro which generates some code. In the callback module, when you invoke use SomeBehaviour, the macro SomeBehaviour.__using__ will be invoked, and the code generated by that macro will be injected into the caller site (i.e. in the place where use SomeBehaviour is invoked).