Введение
Несмотря на то, что Эликсир пытается обеспечить безопасную среду для макросов, основная ответственность за написание чистого кода с помощью макросов ложится на разработчиков. Писать макросы сложнее, чем обычные функции на Эликсире, и соответственно их не стоит использовать, когда в них нет нужды. Так что пишите и применяйте макросы со всей ответственностью.
Эликсир и так предоставляет механизмы для написания обычного кода простым и понятным способом, используя встроенные структуры данных и функции. Следовательно, макросы должны использоваться исключительно в крайнем случае. Помните, что явное лучше, чем неявное. Чистый код лучше, чем сокращённый.
Наш первый макрос
Макросы в Эликсире определяются через defmacro/2
.
В этой главе мы будет использовать файлы вместо запуска примеров кода в IEx. Всё потому, что эти примеры будут в несколько строк кода, поэтому их ввод в IEx будет непродуктивным. Следовательно, вы должны иметь возможность проверять эти примеры, записывая их в файл
macros.exs
, и запускать с помощью командelixir macros.exs
илиiex macros.exs
.
Чтобы лучше понять, как работают макросы, давайте сперва создадим новый модуль, в котором реализуем условный оператор unless
, который делает противоположный if
, как макросом, так и функцией:
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
. Однако, как мы узнали в предыдущей главе, макрос будет получать маскирующие выражения, вводить их в конструкцию quote
и, в конце, возвращать другое маскирующее выражение.
Давайте начнём с 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
.
Другими словами, при вызове:
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
в Эликсире реализуется как макрос:
defmacro unless(clause, do: expression) do
quote do
if(!unquote(clause), do: unquote(expression))
end
end
Конструкции, вроде unless/2
, defmacro/2
, def/2
, defprotocol/2
и многие другие, использующиеся в этом руководстве с самого начала, реализованы на чистом Эликсире, часто в качестве макроса. Это означает, что конструкторы, использующиеся для создания языка, могут использоваться разработчиками для расширения предметно-ориентированного языка, над которым они работают.
Мы может определить любые необходимые нам функции и макросы, в том числе и те, которые переопределяют встроенные определения, предоставляемые самим Эликсиром. Единственным исключением являются специальные формы Эликсира, которые не реализованы в нём и поэтому не могут быть переопределёнными априори полный список специальных форм доступен в модуле Kernel.SpecialForms
.
Гигиена макросов
Макросы в Эликсире выполняются в последнюю очередь. Это гарантирует переменным, определённым внутри quote то, что они не будут конфликтовать с переменными, определёнными в контексте, где этот макрос расширяется. Например:
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
. Если макрос хочет явно повлиять на контекст, он может воспользоваться var!
:
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
Переменная гигиена работает только лишь потому, что Эликсир комментирует переменные с их контекстом. Например, переменная 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
. Именно поэтому Эликсир рассматривает эти две переменные как исходящие из разных контекстов и, следовательно, обрабатывает их соответственно.
Эликсир предоставляет аналогичные механизмы для импортов и псевдонимов. Это гарантирует, что макрос будет вести себя так, как указано в его исходном модуле, не противореча заданному модулю, в котором макрос расширяется. При этом, гигиена может быть обойдена в определённых ситуациях, если использовать макросы, такие как var!/2
и alias!/2
, несмотря на то, что при их использовании необходимо соблюдать осторожность при явном изменении пользовательской среды.
Иногда имена переменных могут быть созданы динамически. В таких случаях 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.expand_once/2
ранее в этой главе, мы использовали специальную форму __ENV__
.
__ENV__
возвращает экземпляр структуры 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
, а также узнать больше о среде компиляции в документации для Macro.Env
.
Приватные макросы
Эликсир также поддерживает приватные макросы через defmacrop
. В качестве приватных функций эти макросы доступны только в модуле, который их определяет, и только во время компиляции.
Важно, чтобы такой макрос был определён до его непосредственного использования. Невозможность определить макрос до его вызова вызовет ошибку во время выполнения, поскольку макрос не будет расширяться и будет переведен в вызов функции:
iex> defmodule Sample do
...> def four, do: two + two
...> defmacrop two, do: 2
...> end
** (CompileError) iex:2: function two/0 undefined
Пишите макросы ответственно
Макросы являются мощной конструкцией, и Эликсир предоставляет множество механизмов для их ответственного использования.
-
Макросы гигиеничны: по умолчанию, переменные, определённые внутри макроса, не будут влиять на код пользователя. Кроме того, вызовы функций и псевдонимы, доступные в контексте макросов, не будут протекать в контексте пользователя.
-
Макросы лексические: невозможно вводить код или макросы глобально. Чтобы использовать макрос, вам необходимо явно задать
require
илиimport
модулю, который определяет макрос. -
Макросы явны: невозможно запустить макрос без его явного вызова. Например, некоторые языки позволяют разработчикам полностью переписывать функции скрыто, часто с помощью синтаксических преобразований или с помощью некоторых механизмов рефлексии. В Эликсире макрос должен быть явно вызван в вызывающем макросе во время компиляции.
-
Язык макросов ясен: многие языки предоставляют синтаксический сахар для
quote
иunquote
. В Эликсире же мы предпочли, чтобы они были изложены явным образом, дабы чётко разграничить определение макроса и его маскирующие выражения.
Даже при таких гарантиях, разработчик играет весомую роль при написании макросов ответственно. Если вы уверены, что вам нужно прибегнуть к макросам, всегда помните, что они не являются вашим 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
# Оставьте необходимый минимум,
# а остальное перенесите в функции
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, в котором показано, как мы можем смешивать макросы и атрибуты модуля воедино для аннотации и расширения модулей и функций.