Исходный код Макросы
Несмотря на то, что 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**, которое показывает, как мы можем комбинировать макросы и атрибуты модулей для аннотации и расширения модулей и функций.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/macros.html