Это руководство познакомит вас с техниками метапрограммирования в Эликсире. Возможность представлять программу на Эликсире своими собственными структурами данных лежит в основе метапрограммирования. Эта глава начинается с изучения этих структур и взаимодействующих между собой конструкций quote
и unquote
. Благодаря этому, в следующей главе мы сможем познакомиться с макросами и наконец создать свой собственный предметно-ориентированный язык.
Quoting
Строительными блоками любой программы на Эликсире являются кортежи с тремя элементами. Например, вызов функции sum(1, 2, 3)
представлен внутренне как:
{:sum, [], [1, 2, 3]}
Вы можете получить представление любого выражения, используя макрос quote
:
iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}
Первый элемент – это имя функции, второй – список ключевых слов, содержащий матаданные, а третий – список аргументов.
Также, в виде таких кортежей, представлены операторы:
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
Даже словарь представляется как вызов %{}
:
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
Переменные тоже представлены с использованием таких триплетов, за исключением лишь последнего элемента, который является атомом, а не списком:
iex> quote do: x
{:x, [], Elixir}
При маскировании более сложных выражений мы можем заметить, что код представлен в таких кортежах, которые часто вложены друг в друга, создавая тем самым структуру, напоминающую дерево. Многие языки назвали бы такое представление абстрактным синтаксическим деревом. Эликсир же называет их маскирующими выражениями:
iex> quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}
Иногда при работе с маскирующими выражениями может оказаться полезным вернуть код текстом. Что ж, это можно сделать с помощью функции Macro.to_string/1
:
iex> Macro.to_string(quote do: sum(1, 2 + 3, 4))
"sum(1, 2 + 3, 4)"
В целом, кортежи более структурированы в соответствии со следующим форматом:
{atom | tuple, list, list | atom}
- Первый элемент – это атом или другой кортеж в том же представлении;
- Второй элемент – это список ключевых слов, содержащий метаданные, вроде чисел и контекстов;
- Третий элемент – это либо список аргументов для вызова функции, либо атом. Когда этот элемент является атомом, это значит, что кортеж представляет из себя переменную.
Помимо кортежа, определенного выше, существует также пять литералов Эликсира, которые при маскировке возвращаюся сами (вместо кортежа). Вот они:
:sum #=> Атомы
1.0 #=> Числа
[1, 2] #=> Списки
"strings" #=> Строки
{key, value} #=> Кортежи с двумя элементами
В основном, код на Эликсире переводится прямо в маскирующие выражения. Мы рекомендуем вам попробовать запустить различные примеры кода и посмотреть на получаемые результаты. Например, что означает String.upcase("foo")
? Мы также узнали, что if(true, do: :this, else: :that)
– это то же самое, что и if true do :this else :that end
. Как это утверждение применяется в маскирующих выражениях?
Unquoting
Конструкция quote
– это получение внутреннего представления какого-то конкретого фрагмента кода. Однако, иногда нам может потребоваться ввести какой-нибудь другой фрагмент кода внутрь представления, которое мы хотим получить.
Для примера, представьте, что у вас есть некая переменная number
, содержащая номер, который вы хотите ввести в маскирующее выражение.
iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"
Это не то, что мы хотели увидеть, так как значение переменной number
не было введено, number
был лишь замаскирован в выражении. Чтобы на самом деле ввести значение переменной number
, в маскирующем представлении должен использоваться unquote
:
iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"
unquote
может даже использоваться для ввода имён функций:
iex> fun = :hello
iex> Macro.to_string(quote do: unquote(fun)(:world))
"hello(:world)"
В некоторых случаях нам может потребоваться ввести много значений внутрь списка. Например, представьте, что у вас есть список, содержащий [1, 2, 6]
и мы хотим ввести в него [3, 4, 5]
. Использование unquote
не даст желаемого результата:
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]"
Именно в такие моменты unquote_splicing
приходит к нам на помощь:
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"
Конструкция unquote
очень полезна при работе с макросами. Дело здесь в том, что при написании макросов разработчики могут получать необходимые куски кода и внедрять их в другие фрагменты программы, которые, в свою очередь, могут быть использованы для преобразования исходного кода или записи нового, генерирующего новый код во время компиляции.
Экранирование
Как мы могли заметить в начале этой главы, только некоторые значения могут быть допустимыми маскирующими выражениями в Эликсире. Например, словарь не является допустимым маскирующим выражением. Также, нет кортежа с четырьмя элементами. Однако такие значения могут быть выражены как маскирующие выражения:
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
В некоторых случаях вам может потребоваться ввести такие значения в маскирующие выражения. Чтобы сделать это, нам необходимо сперва избежать этих значений в маскирующих выражениях с помощью функции Macro.escape/1
:
iex> map = %{hello: :world}
iex> Macro.escape(map)
{:%{}, [], [hello: :world]}
Макросы получают маскирующие выражения и должны их же и возвращать. Однако иногда во время выполнения макроса вам может понадобиться поработать со значениями и здесь нам уже нужно будет провести различие между значениями и маскирующими выражениями.
Другими словами, важно провести различие между регулярными значениями Эликсира (вроде списков, словарей, процессов, ссылок и т. д.) и маскирующим выражением. Некоторые значения, такие как: числа, атомы и строки, имеют маскирующее выражение равное самому значению. Другое значение, вроде словаря, должно быть явно преобразовано. И наконец, значения, подобные функциям и ссылкам, вообще не могут быть преобразованы в маскирующее выражение.
Вы можете больше узнать о конструкциях quote
и unquote
в модуле Kernel.SpecialForms
. Документацию для Macro.escape/1
и других функций, связанных с маскирующими выражениями, можно найти в модуле Macros
.
В этом введении мы заложили основу для написания нашего первого макроса, поэтому перейдём же к следующей главе.