Spec-Zone.ru › Elixir 1.17

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

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

Вот что такое макросы. Они заключаются в получении цитированных выражений и преобразовании их в что-то другое. Фактически, 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.34.1) для язык программирования Elixir

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

Spec-Zone.ru

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