Os behaviours permitem-nos definir, num módulo de behaviour, interfaces (conjuntos de funções e macros) que podem mais tarde ser implementadas por diferentes módulos de callback. Graças à interface partilhada, esses módulos de callback podem ser usados de forma intercambiável.
Repara na grafia britânica de "behaviours".
Para definir um behaviour, temos de criar um novo módulo e especificar uma lista de funções que fazem parte da interface pretendida. Cada função tem de ser definida com o atributo de módulo @callback. A sintaxe é idêntica à de uma especificação de tipo de função (@spec). Temos de especificar o nome da função, uma lista de tipos de argumentos e todos os possíveis tipos de retorno.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Para adicionar um behaviour existente ao nosso módulo (criar um módulo de callback), usamos o atributo de módulo @behaviour. O seu valor deve ser o nome do módulo de behaviour que estamos a adicionar.
Depois, temos de definir todas as funções (callbacks) exigidas por esse módulo de behaviour. Se estivermos a implementar o behaviour de outra pessoa, como os behaviours integrados do Elixir Access ou GenServer, encontramos a lista de todos os callbacks do behaviour na documentação em hexdocs.pm.
Um módulo de callback não está limitado a implementar apenas as funções que fazem parte do seu behaviour. Também é possível que um único módulo implemente vários behaviours.
Para assinalar que função vem de que behaviour, devemos usar o atributo de módulo @impl antes de cada função. O seu valor deve ser o nome do módulo de behaviour 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
Ao definir um behaviour, é possível fornecer uma implementação predefinida de um callback. Esta implementação deve ser definida na expressão citada da macro __using__/1. Para que os utilizadores do módulo de behaviour possam substituir a implementação predefinida, chama a macro defoverridable/1 depois da implementação da função. Aceita uma lista de palavras-chave com nomes de funções como chaves e aridades de funções 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
Repara que definir funções dentro de __using__/1 é desaconselhado para qualquer outro fim que não seja definir implementações predefinidas de callbacks, mas podes sempre definir funções noutro módulo e importá-las na macro __using__/1.