Behaviours in Elixir, which macro to use ? ( use VS behaviour? )

Background

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

  1. Why am I not using the @bahviour macro here and use use instead?
  2. What are the differences between the two?
  3. When should I use one or the other?
  4. Why do I have to use use with GenServer instead of @behaviour ?

There is a similar topic here that could be helpful.

You do not have to.

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.

3 Likes

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.

In some cases you don’t even need @behaviour.

For example with GenServer it is enough implement a simple module with the callback handlers that don’t have a default implementation and then simply specify the (callback) module name when using GenServer.start_link/3 (something that only became apparent to me when I read Replacing GenEvent by a Supervisor + GenServer).

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).

Also keep in mind that behaviours are an Erlang/OTP concept while module attributes and hygienic macros only exist in the Elixir space (Erlang has it’s own style of macros).

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:

@spec apply(m, function_name, args) :: any() when m: module(), function_name: atom(), args: [any()]

i.e.

  • the behaviour module establishes the convention which function_names will be called with what type of arguments args.
  • the callback module m implements the function_names which are capable of processing the args as supplied by the behaviour module.
1 Like

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).

Hi,

Can you help us with a small example. I am no really clear about this and went back to your book to study further and found OTP Behavior only.

Thanks,

I can give it a try, but first I need more details. What exactly needs clarification, i.e. which part do you find confusing?

Thanks,

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?

Thanks in advance for the clarification.

Best regards,

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).

6 Likes

Thank you for the example.

I will definitely give it a try.

Best regards,

1 Like