Треки
/
Elixir
Elixir
/
Вправи
/
Танцюючі крапки
Танцюючі крапки

Танцюючі крапки

Навчальна вправа

Вступ

Використання

Макрос 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. Якщо їх не вказати явно, типовим значенням є порожній список.

Поведінки

Поведінки дають нам змогу визначати інтерфейси (набори функцій і макросів) у модулі поведінки, які згодом можуть реалізувати різні модулі-колбеки. Завдяки спільному інтерфейсу ці модулі-колбеки можна використовувати взаємозамінно.

Note

Звернімо увагу на британське написання слова «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.

Вказівки

Друг, який прагне стати митцем, звернувся до нас з ідеєю проєкту. Поєднаймо його творчу уяву з нашими технічними знаннями. Час узятися за генеративне мистецтво!

Обмеження допомагають творчості й скорочують терміни проєкту, тож ми обидва домовилися обмежити наш шедевр однією формою - колом. Але кіл буде багато. І вони можуть рухатися! Назвімо це... «танцювальні крапки».

Друг неодмінно захоче вигадувати нові складні рухи для крапок, тож почнімо кодувати зі створення архітектури, яка згодом дасть змогу легко визначати нові анімації.

1. Визначте поведінку анімації

Кожен модуль анімації має реалізувати два колбеки: init/1 і handle_frame/3. Визначте їх у модулі Animation.

Визначте колбек init/1. Він має приймати один аргумент типу opts і повертати або кортеж {:ok, opts}, або кортеж {:error, error}. Реалізації цього колбека перевірятимуть, чи надані опції придатні для цього конкретного типу анімації.

Визначте колбек handle_frame/3. Він має приймати три аргументи: крапку, номер кадру й опції. Він завжди має повертати крапку. Реалізації цього колбека змінюватимуть атрибути крапки на основі поточного номера кадру й опцій анімації.

2. Надайте типову реалізацію колбека 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]}

3. Реалізуйте анімацію 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}

4. Реалізуйте анімацію 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}
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Elixir Exercism

Час розпочати Танцюючі крапки?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Elixir, а також 58 концепцій168 вправ та справжнє наставництво від людей, і все це безкоштовно.