Spec-Zone.ru › Elixir 1.16

Source Макросы

Несмотря на то, что Elixir старается предоставить безопасную среду для макросов, основная ответственность за написание чистого кода с макросами ложится на разработчиков. Макросы сложнее писать, чем обычные функции Elixir, и считается плохим стилем использовать их, когда они не нужны. Пишите макросы ответственно.

Elixir уже предоставляет механизмы для написания повседневного кода простым и читаемым способом с использованием его структур данных и функций. Макросы следует использовать только в крайнем случае. Помните, что **явное лучше неявного**. **Ясный код лучше лаконичного кода.**

Наш первый макрос

Макросы в Elixir определяются с помощью defmacro/2.

В этом руководстве мы будем использовать файлы вместо запуска примеров кода в IEx. Это связано с тем, что примеры кода будут охватывать несколько строк кода, и их ввод в IEx может быть неэффективным. Вы должны иметь возможность запускать примеры кода, сохранив их в macros.exs файл и запустив его с помощью elixir macros.exs или iex macros.exs.

Для лучшего понимания работы макросов давайте создадим новый модуль, где мы реализуем unless (который делает обратное if/2), как макрос и как функцию:

defmodule Unless do
  def fun_unless(clause, do: expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

Функция получает аргументы и передает их if/2. Однако, как мы узнали в предыдущем руководстве, макрос получит цитированные выражения, вставит их в цитату и, наконец, вернёт другую цитированную выражение.

Давайте начнём iex с модуля выше:

$ iex macros.exs

и поиграем с этими определениями:

iex> require Unless
iex> Unless.macro_unless(true, do: IO.puts "this should never be printed")
nil
iex> Unless.fun_unless(true, do: IO.puts "this should never be printed")
"this should never be printed"
nil

В нашей макрос-реализации предложение не было напечатано, хотя оно было напечатано в нашей функциональной реализации. Это потому, что аргументы вызова функции оцениваются до вызова функции. Однако макросы не оценивают свои аргументы. Вместо этого они получают аргументы в виде цитированных выражений, которые затем преобразуются в другие цитированные выражения. В этом случае мы переписали наш unless макрос, чтобы он стал if/2 за кулисами.

Другими словами, при вызове:

Unless.macro_unless(true, do: IO.puts "this should never be printed")

Наш macro_unless макрос получил следующее:

macro_unless(true, [do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["this should never be printed"]}])

и затем он вернул цитированное выражение следующим образом:

{:if, [],
 [{:!, [], [true]},
  [do: {{:., [],
     [{:__aliases__,
       [], [:IO]},
      :puts]}, [], ["this should never be printed"]}]]}

Мы можем фактически проверить, что это так, используя Macro.expand_once/2:

iex> expr = quote do: Unless.macro_unless(true, do: IO.puts("this should never be printed"))
iex> res  = Macro.expand_once(expr, __ENV__)
iex> IO.puts(Macro.to_string(res))
if(!true) do
  IO.puts("this should never be printed")
end
:ok

Macro.expand_once/2 получает цитированное выражение и расширяет его в соответствии с текущей средой. В этом случае он расширил/вызвал Unless.macro_unless/2 макрос и вернул результат. Затем мы преобразовали возвращённое цитированное выражение в строку и напечатали её (мы поговорим о __ENV__ позже в этой главе).

Вот в чём суть макросов. Они заключаются в получении цитированных выражений и преобразовании их в нечто иное. Фактически, unless/2 в Elixir реализован как макрос:

defmacro unless(clause, do: expression) do
  quote do
    if(!unquote(clause), do: unquote(expression))
  end
end

Конструкции, такие как unless/2, defmacro/2, def/2, defprotocol/2 и многие другие, используемые в стандартной библиотеке Elixir, написаны на чистом Elixir, часто как макрос. Это означает, что используемые конструкции для построения языка могут использоваться разработчиками для расширения языка в областях, над которыми они работают.

Мы можем определить любые функции и макросы, которые нам нужны, включая те, которые перезаписывают встроенные определения, предоставляемые Elixir. Исключение составляют специальные формы Elixir, которые не реализованы в Elixir и поэтому не могут быть перезаписаны. Полный список специальных форм доступен в Kernel.SpecialForms.

Гигиена макросов

Макросы Elixir имеют «позднее разрешение». Это гарантирует, что переменная, определённая внутри цитаты, не будет конфликтовать с переменной, определённой в контексте, где этот макрос расширяется. Например:

defmodule Hygiene do
  defmacro no_interference do
    quote do: a = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.no_interference()
    a
  end
end

HygieneTest.go()
# => 13

В приведённом выше примере, даже если макрос вставляет a = 1, он не влияет на переменную a, определённую функцией go/0. Если макрос хочет явно повлиять на контекст, он может использовать var!/1:

defmodule Hygiene do
  defmacro interference do
    quote do: var!(a) = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.interference()
    a
  end
end

HygieneTest.go()
# => 1

Приведённый выше код будет работать, но выведет предупреждение: variable "a" is unused. Макрос перезаписывает исходное значение, и исходное значение никогда не используется.

Гигиена переменных работает только потому, что Elixir аннотирует переменные их **контекстом**. Например, переменная x , определённая в строке 3 модуля, будет представлена как:

{:x, [line: 3], nil}

Однако цитированная переменная будет представлена как:

defmodule Sample do
  def quoted do
    quote do: x
  end
end

Sample.quoted() #=> {:x, [line: 3], Sample}

Обратите внимание, что третий элемент в цитированной переменной — это атом Sample, а не nil, который отмечает переменную как происходящую из модуля Sample. Таким образом, Elixir рассматривает эти две переменные как происходящие из разных контекстов и обрабатывает их соответствующим образом.

Elixir предоставляет аналогичные механизмы для импорта и псевдонимов тоже. Это гарантирует, что макрос будет вести себя так, как указано в модуле источника, а не конфликтовать с целевым модулем, где макрос расширяется. Гигиена может быть обойдена в определённых ситуациях с помощью макросов, таких как var!/2 и alias!/1, хотя следует проявлять осторожность при их использовании, так как они непосредственно изменяют пользовательскую среду.

Иногда имена переменных могут создаваться динамически. В таких случаях Macro.var/2 можно использовать для определения новых переменных:

defmodule Sample do
  defmacro initialize_to_char_count(variables) do
    Enum.map(variables, fn name ->
      var = Macro.var(name, nil)
      length = name |> Atom.to_string() |> String.length()

      quote do
        unquote(var) = unquote(length)
      end
    end)
  end

  def run do
    initialize_to_char_count([:red, :green, :yellow])
    [red, green, yellow]
  end
end

> Sample.run() #=> [3, 5, 6]

Обратите внимание на второй аргумент Macro.var/2. Это используемый **контекст**, который будет определять гигиену, как описано в следующей секции. Также ознакомьтесь с Macro.unique_var/2 в случаях, когда вам нужно генерировать переменные с уникальными именами.

Среда

При вызове Macro.expand_once/2 ранее в этой главе, мы использовали специальную форму __ENV__/0.

__ENV__/0 возвращает структуру Macro.Env, которая содержит полезную информацию о среде компиляции, включая текущий модуль, файл и строку, все переменные, определённые в текущем области видимости, а также импорты, требования и многое другое:

iex> __ENV__.module
nil
iex> __ENV__.file
"iex"
iex> __ENV__.requires
[IEx.Helpers, Kernel, Kernel.Typespec]
iex> require Integer
nil
iex> __ENV__.requires
[IEx.Helpers, Integer, Kernel, Kernel.Typespec]

Многие функции в модуле Macro ожидают среду Macro.Env. Вы можете узнать больше об этих функциях в Macro и узнать больше о среде компиляции в Macro.Env.

Закрытые макросы

Elixir также поддерживает **закрытые макросы** посредством defmacrop. Как и закрытые функции, эти макросы доступны только внутри модуля, который их определяет, и только во время компиляции.

Важно, чтобы макрос был определён до его использования. Если макрос не определён перед его вызовом, произойдёт ошибка во время выполнения, так как макрос не будет расширен и будет преобразован в вызов функции:

iex> defmodule Sample do
...>  def four, do: two() + two()
...>  defmacrop two, do: 2
...> end
** (CompileError) iex:2: function two/0 undefined

Пишите макросы ответственно

Макросы — мощная конструкция, и Elixir предоставляет множество механизмов, чтобы обеспечить их ответственное использование.

  • Макросы **гигиеничны**: по умолчанию переменные, определённые внутри макроса, не повлияют на код пользователя. Кроме того, вызовы функций и псевдонимы, доступные в контексте макроса, не будут просачиваться в пользовательский контекст.

  • Макросы **лексические**: невозможно внедрить код или макросы глобально. Для использования макроса необходимо явно require или import модуль, который определяет макрос.

  • Макросы **явные**: невозможно запустить макрос без явного вызова. Например, некоторые языки позволяют разработчикам полностью переписывать функции за кулисами, часто с помощью преобразований парсеров или с помощью некоторых механизмов рефлексии. В Elixir макрос должен быть явно вызван в вызывающей стороне во время компиляции.

  • Язык макросов понятен: многие языки предоставляют синтаксические сокращения для quote и unquote. В Elixir мы предпочли иметь их явно прописанными, чтобы чётко разграничить границы определения макроса и его цитированных выражений.

Даже с такими гарантиями, разработчик играет большую роль при написании макросов ответственно. Если вы уверены, что вам нужно прибегнуть к макросам, помните, что макросы не являются вашим API. Держите определения макросов короткими, включая их цитированное содержимое. Например, вместо написания такого макроса:

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      do_this(unquote(a))
      # ...
      do_that(unquote(b))
      # ...
      and_that(unquote(c))
    end
  end
end

напишите:

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      # Keep what you need to do here to a minimum
      # and move everything else to a function
      MyModule.do_this_that_and_that(unquote(a), unquote(b), unquote(c))
    end
  end

  def do_this_that_and_that(a, b, c) do
    do_this(a)
    ...
    do_that(b)
    ...
    and_that(c)
  end
end

Это делает ваш код более понятным и простым в тестировании и обслуживании, поскольку вы можете вызвать и протестировать do_this_that_and_that/3 напрямую. Это также помогает вам разработать фактическое API для разработчиков, которые не хотят полагаться на макросы.

На этом мы завершаем наше введение в макросы. Следующее руководство — краткое обсуждение **DSL**, которое покажет, как мы можем комбинировать макросы и атрибуты модуля для аннотации и расширения модулей и функций.

← Предыдущая страница Цитата и условное снятие цитаты
Следующая страница → Языки предметной области (DSL)

Скачать версию ePub

Создано с помощью ExDoc (v0.32.2) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/macros.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API