Поведінки дають змогу визначати інтерфейси (набори функцій і макросів) у модулі поведінки, які згодом можуть бути реалізовані різними модулями-колбеками. Завдяки спільному інтерфейсу ці модулі-колбеки можна використовувати взаємозамінно.
Звернімо увагу на британське написання «behaviours».
Щоб визначити поведінку, потрібно створити новий модуль і вказати список функцій, які входять до бажаного інтерфейсу. Кожну функцію потрібно визначити за допомогою атрибута модуля @callback. Синтаксис такий самий, як у специфікації типу функції (@spec). Потрібно вказати назву функції, список типів аргументів і всі можливі типи повернених значень.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Щоб додати наявну поведінку до нашого модуля (створити модуль-колбек), ми використовуємо атрибут модуля @behaviour. Його значенням має бути назва модуля поведінки, яку ми додаємо.
Потім потрібно визначити всі функції (колбеки), які вимагає цей модуль поведінки. Якщо ми реалізуємо чужу поведінку, наприклад вбудовані поведінки Elixir Access або GenServer, список усіх колбеків цієї поведінки можна знайти в документації на hexdocs.pm.
Модуль-колбек не обмежується реалізацією лише функцій, що є частиною його поведінки. Один модуль також може реалізувати кілька поведінок.
Щоб позначити, яка функція походить з якої поведінки, слід використовувати атрибут модуля @impl перед кожною функцією. Його значенням має бути назва модуля поведінки, що визначає цей колбек.
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
Визначаючи поведінку, можна надати типову реалізацію колбека. Цю реалізацію слід визначити в цитованому виразі макроса __using__/1. Щоб користувачі модуля поведінки могли перевизначити типову реалізацію, потрібно викликати макрос defoverridable/1 після реалізації функції. Він приймає ключовий список, у якому ключами є назви функцій, а значеннями - їхні арності.
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
Варто зауважити, що визначати функції всередині __using__/1 не рекомендується для будь-якої іншої мети, ніж визначення типових реалізацій колбеків, але функції завжди можна визначити в іншому модулі та імпортувати їх у макросі __using__/1.