Los comportamientos nos permiten definir interfaces (conjuntos de funciones y macros) en un módulo de comportamiento que luego pueden ser implementadas por diferentes módulos de callback. Gracias a la interfaz compartida, esos módulos de callback se pueden usar de forma intercambiable.
Fíjate en la ortografía británica de «behaviours».
Para definir un comportamiento, necesitamos crear un nuevo módulo y especificar una lista de funciones que forman parte de la interfaz deseada. Cada función debe definirse usando el atributo de módulo @callback. La sintaxis es idéntica a una especificación de tipo de función (@spec). Necesitamos especificar un nombre de función, una lista de tipos de argumentos y todos los tipos de retorno posibles.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Para añadir un comportamiento existente a nuestro módulo (crear un módulo de callback) usamos el atributo de módulo @behaviour. Su valor debe ser el nombre del módulo de comportamiento que estamos añadiendo.
Luego, necesitamos definir todas las funciones (callbacks) que requiere ese módulo de comportamiento. Si estamos implementando el comportamiento de otra persona, como los comportamientos integrados de Elixir Access o GenServer, encontraríamos la lista de todos los callbacks del comportamiento en la documentación en hexdocs.pm.
Un módulo de callback no se limita a implementar solo las funciones que forman parte de su comportamiento. También es posible que un único módulo implemente varios comportamientos.
Para marcar qué función proviene de qué comportamiento, debemos usar el atributo de módulo @impl antes de cada función. Su valor debe ser el nombre del módulo de comportamiento que define este callback.
defmodule BookCollection do
@behaviour Countable
defstruct [:list, :owner]
@impl Countable
def count(collection) do
Enum.count(collection.list)
end
def mark_as_read(collection, book) do
# other function unrelated to the Countable behaviour
end
end
Al definir un comportamiento, es posible proporcionar una implementación predeterminada de un callback. Esta implementación debe definirse en la expresión citada de la macro __using__/1. Para que los usuarios del módulo de comportamiento puedan sobrescribir la implementación predeterminada, llama a la macro defoverridable/1 después de la implementación de la función. Acepta una lista de palabras clave con nombres de funciones como claves y aridades de funciones como valores.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
defmacro __using__(_) do
quote do
@behaviour Countable
def count(collection), do: Enum.count(collection)
defoverridable count: 1
end
end
end
Ten en cuenta que definir funciones dentro de __using__/1 no es recomendable para ningún otro propósito que no sea definir implementaciones de callback predeterminadas, pero siempre puedes definir funciones en otro módulo e importarlas en la macro __using__/1.