Макрос use дає нам змогу швидко розширити наш модуль функціональністю, яку надає інший модуль. Коли ми застосовуємо use до модуля, цей модуль може вставити код у наш модуль: наприклад, визначити функції, виконати import чи alias інших модулів або встановити атрибути модуля.
Якщо ми колись бачили тестові файли деяких вправ з Elixir тут, на Exercism, то, найімовірніше, помітили, що всі вони починаються з use ExUnit.Case. Саме цей один рядок коду робить макроси test і assert доступними в тестовому модулі.
defmodule LasagnaTest do
use ExUnit.Case
test "expected minutes in oven" do
assert Lasagna.expected_minutes_in_oven() === 40
end
end
__using__/1
Що саме відбувається, коли ми застосовуємо use до модуля, визначає макрос __using__/1 цього модуля. Він приймає один аргумент, список ключових слів з опціями, і повертає цитований вираз. Код із цього цитованого виразу вставляється в наш модуль під час виклику use.
defmodule ExUnit.Case do
defmacro __using__(opts) do
# some real-life ExUnit code omitted here
quote do
import ExUnit.Assertions
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
end
end
end
Опції можна передати другим аргументом під час виклику use, наприклад: use ExUnit.Case, async: true. Якщо їх не вказати явно, типовим значенням є порожній список.
Поведінки дають нам змогу визначати інтерфейси (набори функцій і макросів) у модулі поведінки, які згодом можуть реалізувати різні модулі-колбеки. Завдяки спільному інтерфейсу ці модулі-колбеки можна використовувати взаємозамінно.
Звернімо увагу на британське написання слова «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.
Друг, який прагне стати митцем, звернувся до нас з ідеєю проєкту. Поєднаймо його творчу уяву з нашими технічними знаннями. Час узятися за генеративне мистецтво!
Обмеження допомагають творчості й скорочують терміни проєкту, тож ми обидва домовилися обмежити наш шедевр однією формою - колом. Але кіл буде багато. І вони можуть рухатися! Назвімо це... «танцювальні крапки».
Друг неодмінно захоче вигадувати нові складні рухи для крапок, тож почнімо кодувати зі створення архітектури, яка згодом дасть змогу легко визначати нові анімації.
Кожен модуль анімації має реалізувати два колбеки: init/1 і handle_frame/3. Визначте їх у модулі Animation.
Визначте колбек init/1. Він має приймати один аргумент типу opts і повертати або кортеж {:ok, opts}, або кортеж {:error, error}. Реалізації цього колбека перевірятимуть, чи надані опції придатні для цього конкретного типу анімації.
Визначте колбек handle_frame/3. Він має приймати три аргументи: крапку, номер кадру й опції. Він завжди має повертати крапку. Реалізації цього колбека змінюватимуть атрибути крапки на основі поточного номера кадру й опцій анімації.
init/1
Поведінку Animation має бути легко включити в інші модулі, викликавши use DancingDots.Animation.
Щоб це працювало, реалізуйте макрос __using__ у модулі Animation так, щоб він задавав модуль Animation як поведінку іншого модуля. Він також має надавати типову реалізацію колбека init/1. Типова реалізація init/1 має повертати надані опції без змін.
defmodule MyCustomAnimation do
use DancingDots.Animation
end
MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}
Flicker
Використайте поведінку Animation, щоб реалізувати мерехтливу анімацію.
Вона має використовувати типовий колбек init/1, бо не приймає жодних опцій.
Реалізуйте колбек handle_frame/3, який обробляє один кадр. Якщо номер кадру кратний чотирьом, функція має повертати крапку з половиною її початкової непрозорості. В інших кадрах вона має повертати крапку без змін.
Кадри лічаться від 1. Крапка, передана в handle_frame/3, завжди перебуває в початковому стані, а не в стані з попереднього кадру.
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
DancingDots.Flicker.handle_frame(dot, 1, [])
# => %DancingDots.Dot{opacity: 1, radius: 24, x: 100, y: 100}
DancingDots.Flicker.handle_frame(dot, 4, [])
# => %DancingDots.Dot{opacity: 0.5, radius: 24, x: 100, y: 100}
Zoom
Використайте поведінку Animation, щоб реалізувати анімацію масштабування.
Ця анімація приймає одну опцію - швидкість. Швидкість може бути будь-яким числом. Якщо вона відʼємна, крапка зменшується замість того, щоб збільшуватися.
Реалізуйте колбек init/1. Він має перевіряти, що надані опції є списком ключових слів із ключем :velocity. Значення швидкості має бути числом. Якщо це не число, поверніть помилку "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}".
Реалізуйте колбек handle_frame/3. Він має повертати крапку з радіусом, збільшеним на добуток (поточного номера кадру мінус один) і швидкості.
Кадри лічаться від 1. Крапка, передана в handle_frame/3, завжди перебуває в початковому стані, а не в стані з попереднього кадру.
DancingDots.Zoom.init(velocity: nil)
# => {:error, "The :velocity option is required, and its value must be a number. Got: nil"}
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
DancingDots.Zoom.handle_frame(dot, 1, velocity: 10)
# => %DancingDots.Dot{radius: 24, opacity: 1, x: 100, y: 100}
DancingDots.Zoom.handle_frame(dot, 2, velocity: 10)
# => %DancingDots.Dot{radius: 34, opacity: 1, x: 100, y: 100}
Зареєструйтеся на Exercism, щоб вивчати й опановувати Elixir, а також 58 концепцій168 вправ та справжнє наставництво від людей, і все це безкоштовно.