Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Elixir
Code

Основы метапрограммирования в Elixir

by
Length:LongLanguages:

Russian (Pусский) translation by Marat Amerov (you can also view the original English article)

Метапрограммирование - это мощная, но довольно сложная техника, это означает, что программа может анализировать или даже модифицировать себя во время исполнения. Многие современные языки поддерживают эту функцию, а Elixir не является исключением.

С метапрограммированием вы можете создавать новые сложные макросы, динамически определять и откладывать исполнение кода, что позволяет писать более сжатый и мощный код. Это действительно продвинутая тема, но, надеюсь, после прочтения этой статьи вы получите общее представление о том, как начать работу с метапрограммированием в Elixir.

В этой статье вы узнаете:

  • Что такое абстрактное синтаксическое дерево и как код Elixir представлен под капотом.
  • Что означают функции quote и unquote.
  • Что такое макросы и как работать с ними.
  • Как внедрять значения со связыванием.
  • Почему макросы гигиеничны.

Прежде чем начать, позвольте мне дать вам небольшой совет. Помните, что дядя Человека-паука сказал: "С великой силой приходит большая ответственность"? Это может быть применено и к метапрограммированию, потому что это очень мощная фича, которая позволяет вам скручивать и сгибать код по своему желанию.

Тем не менее, вы не должны злоупотреблять им, и вы должны придерживаться более простых решений, когда это разумно и возможно. Слишком много метапрограммирования может сделать ваш код намного сложнее для понимания и поддержки, поэтому будьте осторожны.

Абстрактное синтаксическое дерево и Quote

Первое, что нам нужно понять, - это то, как на самом деле представлен наш Elixir код. Эти представления часто называются абстрактными синтаксическими деревьями (AST), но официальное руководство Elixir рекомендует называть их просто цитируемыми выражениями.

Похоже, что выражения имеют структуру в виде кортежей с тремя элементами. Но как мы можем в этом убедится? Есть функция, называемая quote, которая возвращает внутреннее представление для некоторого переданного кода. В принципе, он превращает код в неисполненную форму. Например:

Так что здесь происходит? Кортеж, возвращаемый функцией quote, всегда имеет следующие три элемента:

  1. Atom или другой кортеж с тем же представлением. В этом случае это атом :+, что означает, что мы выполняем добавление. Кстати, эта форма написания операций должна быть знакома, если вы пришли из мира Ruby.
  2. Список ключевых слов с метаданными. В этом примере мы видим, что модуль Kernel был импортирован для нас автоматически.
  3. Список аргументов или атом. В этом случае это список с аргументами 1 и 2.

Конечно, представление может быть намного сложнее:

С другой стороны, некоторые литералы возвращают себя во внутреннем представлении, а именно:

  • атомы
  • Числа
  • числа с плавающей точкой
  • списки
  • сткоки
  • кортежи (только с двумя элементами!)

В следующем примере мы видим, что передача атома в quote возвращает этот атом:

Теперь, когда мы знаем, как код представлен под капотом, перейдем к следующему разделу и посмотрим, что такое макросы и почему выражения переведенные во внутреннее представление важны.

Макросы

Макросы - это специальные формы, такие как функции, но те, которые возвращают код во внутреннем представлении. Затем этот код помещается в приложение, и его исполнение откладывается. Интересно, что макросы также не исполняют переданные им параметры, они также представлены в качестве quoted выражений. Макросы могут использоваться для создания пользовательских сложных функций, используемых во всем вашем проекте.

Однако, имейте в виду, что макросы более сложны, чем обычные функции, и в официальном руководстве говорится, что они должны использоваться только в при необходимости. Другими словами, если вы можете использовать функцию, не создавайте макрос, потому что таким образом ваш код становится излишне сложным и, тем более, сложнее поддерживаемым. Тем не менее, макросы имеют свои варианты использования, поэтому давайте посмотрим, как их создать.

Все начинается с вызова defmacro (который фактически является макросом):

Этот макрос просто принимает аргумент и печатает его.

Кроме того, стоит упомянуть, что макросы могут быть приватными, как функции. Приватные макросы можно вызывать только из модуля, где они были определены. Чтобы определить такой макрос, используйте defmacrop.

Теперь давайте создадим отдельный модуль, который будет использоваться в качестве нашей песочницы:

Когда вы запустите этот код, будут напечатаны {:{}, [line: 11], [1, 2, 3]}, что на самом деле означает, что аргумент имеет форму внутреннего представления (unevaluated). Однако, прежде чем продолжить, позвольте мне сделать небольшую заметку.

Require

Почему мы создали два отдельных модуля: один для определения макроса и другой для запуска кода примера? Похоже, что мы должны сделать это так, потому что макросы обрабатываются до запуска программы. Мы также должны убедиться, что определенный макрос доступен в модуле, и это делается с помощью require. Эта функция, в основном, гарантирует, что данный модуль скомпилирован до текущего.

Вы можете спросить, почему мы не можем избавиться от  модуля Main? Давайте попробуем сделать это:

К сожалению, мы получаем сообщение о том, что функция test не найдена, хотя есть макрос с тем же именем. Это происходит потому, что модуль MyLib определен в той же области (и в том же файле), где мы пытаемся ее использовать. Это может показаться немного странным, но на данный момент просто помните, что для предотвращения подобных ситуаций должен быть создан отдельный модуль.

Также обратите внимание, что макросы нельзя использовать глобально: сначала вы должны импортировать или реквайрить соответствующий модуль.

Макросы и выражения во внутреннем представлении

Таким образом, мы знаем, как выражения Elixir представлены внутри и что такое макросы...  Что теперь? Итак, теперь мы можем использовать эти знания и посмотреть, как код во внутреннем представлении может быть исполнен.

Вернемся к нашим макросам. Важно знать, что последним выражением любого макроса считается код во внутреннем представлении, который будет исполнятся и возвращаться автоматически при вызове макроса. Мы можем переписать пример из предыдущего раздела, переместив IO.inspect в модуль Main:

Видите, что происходит? Кортеж, возвращенный макросом, не является кодом во внутреннем представлении, но исполняется! Вы можете попробовать добавить два целых числа:

Еще раз, код был выполнен, и было возвращено 3. Мы даже можем попытаться использовать функцию quote напрямую, и последняя строка будет по-прежнему исполняться:

arg является кодом во внутреннем представлении (отметим, кстати, что мы даже можем видеть номер строки, где был вызван макрос), но для нас исполнялось  выражение во внутреннем представлении с кортежем {1,2,3}, так как это последняя строка макроса.

У нас может возникнуть соблазн попытаться использовать arg в математическом выражении:

Но это вызовет ошибку, говорящую, что arg не существует. Почему так? Это потому, что arg буквально вставлен в строку, которую мы передаем в quote. Но вместо этого мы должны исполнить arg, вставить результат в строку и затем выполнить quote. Для этого нам понадобится еще одна функция, называемая unquote.

Перевод кода из внутреннего представления

unquote - это функция, которая вводит результат исполнения кода во внутреннем представлении. Это может показаться немного странным, но на самом деле все довольно просто. Давайте возьмем предыдущий пример кода:

Теперь наша программа вернет 4, что мы и хотели! Случается, что код, переданный в функцию unquote, запускается только тогда, когда выполняется код во внутреннем представлении, а не когда он сначала разбирается.

Давайте рассмотрим несколько более сложный пример. Предположим, мы хотели бы создать функцию, которая запускает некоторое выражение, если данная строка является palindrome. Мы могли бы написать что-то вроде этого:

Суффикс _f здесь означает, что это функция для которой позже мы создадим макрос. Однако, если мы попытаемся запустить эту функцию сейчас, текст будет распечатан, даже если строка не является палиндром:

Аргументы, переданные функции, исполняются до того, как функция фактически вызывается, поэтому мы видим, что строка "yes" напечатана на экране. Это действительно не то, чего мы хотим достичь, поэтому давайте попробуем использовать макрос:

Здесь мы переводим код во внутреннее представление, содержащий условие if, и используем unquote внутри для оценки значений аргументов при фактическом вызове макроса. В этом примере на экран ничего не будет напечатано, что правильно!

Инъекция значений с привязкой

Использование unquote - не единственный способ вставить код во внутреннее представление. Мы также можем использовать функцию, называемую binding. Фактически, это просто опция, переданная функции quote, которая принимает список ключевых слов со всеми переменными, которые должны быть обработанны только раз.

Чтобы выполнить привязку, передайте bind_quoted в функцию quote следующим образом:

Это может пригодиться, если вы хотите, чтобы выражение, используемое в нескольких местах, исполнялось только один раз. Как показано в этом примере, мы можем создать простой макрос, который выводит строку дважды с задержкой в ​​две секунды:

Теперь, если вы вызываете его, передавая системное время, две строки будут иметь тот же результат:

Это не относится к unquote, потому что аргумент будет оцениваться дважды с небольшой задержкой, поэтому результаты не будут одинаковыми:

Преобразование кода во внутреннем представлении

Иногда вы, возможно, захотите понять, как выглядит ваш  код который конвертирован во внутреннее представление, например, для его отладки. Это можно сделать, используя функцию to_string:

Распечатанная строка:

Мы видим, что переданный аргумент str был исполнен, и результат был вставлен прямо в код. \n здесь означает "новая строка".

Кроме того, мы можем расширить код во внутреннем представлении с помощью expand_once и expand:

Что производит:

Конечно, это представление можно вернуть обратно к строке:

Мы получим тот же результат, что и раньше:

Функция expand более сложна, так как она пытается развернуть каждый макрос в переданном коде:

Результатом будет:

Мы видим этот результат, потому что if на самом деле сам является макросом и полагается на оператор case, он также расширен.

В этих примерах __ENV__ - это специальная форма, которая возвращает информацию об окружающей среде, такую ​​как текущий модуль, файл, строка, переменная в текущей области и импорты.

Макросы являются гигиеничными

Возможно, вы слышали, что макросы на самом деле гигиеничны. Это означает, что они не перезаписывают какие-либо переменные за пределами их скоупа. Чтобы доказать это, добавим образец переменной, попробуйте изменить его значение в разных местах, а затем выведите его:

Таким образом, other_var получил значение внутри функции start! , внутри макроса и внутри quote. Вы увидите следующий результат:

Это означает, что наши переменные независимы, и мы не вводим никаких конфликтов, используя одно и то же имя везде (хотя, конечно, было бы лучше избегать такого подхода).

Если вам действительно нужно изменить внешнюю переменную из макроса, вы можете использовать var! как здесь:

Используя var!, мы фактически говорим, что данная переменная не должна быть гигиеничной. Однако будьте осторожны при использовании этого подхода, поскольку вы можете потерять информацию о том, что там переписывается.

Заключение

В этой статье мы обсудили основы метапрограммирования на языке Elixir. Мы рассмотрели использование quote, unquote, макросов и привязок при просмотре некоторых примеров и случаев использования. На этом этапе вы готовы применить эти знания на практике и создать более сжатые и мощные программы. Помните, однако, что обычно лучше иметь понятный код, чем сжатый код, поэтому не злоупотребляйте метапрограммированием в ваших проектах.

Если вы хотите больше узнать о функциях, которые я описал, не стесняйтесь читать официальную документацию о макросах, quote и ​​unquote. Я действительно надеюсь, что эта статья дала вам хорошее введение в метапрограммирование в Elixir, которое действительно может показаться довольно сложным вначале. Во всяком случае, не бойтесь экспериментировать с этими новыми инструментами!

Я благодарю вас за то, что оставались со мной, и скоро увидимся.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.