Los comportamientos nos permiten definir interfaces (conjuntos de funciones y macros) en un módulo de comportamiento que después pueden implementar distintos 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, tenemos que crear un módulo nuevo y especificar una lista de las funciones que forman parte de la interfaz deseada. Cada función debe definirse con el atributo de módulo @callback. La sintaxis es idéntica a la de un typespec de función (@spec). Hay que especificar el nombre de la función, una lista de tipos de argumento y todos los posibles tipos de retorno.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Para agregar 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 agregando.
Después, debemos definir todas las funciones (callbacks) que requiere ese módulo de comportamiento. Si estamos implementando el comportamiento de otra persona, como los comportamientos Access o GenServer integrados en Elixir, encontraríamos la lista de todos los callbacks del comportamiento en la documentación de 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 solo módulo implemente varios comportamientos.
Para indicar de qué comportamiento proviene cada función, 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 quienes usan el 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 no se recomienda definir funciones dentro de __using__/1 para ningún propósito que no sea definir implementaciones predeterminadas de callbacks, pero siempre puedes definir funciones en otro módulo e importarlas en la macro __using__/1.