Spec-Zone.ru › Elixir 1.18

Исходный код Макросы

Несмотря на то, что 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. Однако, как мы узнали в предыдущем руководстве 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__ позже в этой главе).

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

defmacro if(clause, do: expression) do
  quote do
    case clause do
      x when x in [false, nil] -> nil
      _ -> unquote(expression)
  end
end

Конструкции, такие как if/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.36.1) для языка программирования Elixir

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

Spec-Zone.ru

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