Критика чистого макроса
Как я уже писал ранее в другом посте, я являюсь поклонником языка программирования Elixir. Не скрою, что я заинтересован в расширении числа сторонников Elixir всеми возможными средствами и приёмами, пусть даже путём полемики и здоровой критики её отцов основателей.
Хотя Elixir и молодой язык, но видно, что разработчики языка уделяют большое внимание просветительской работе и изданию разноплановых учебников. С удовольствием читаю и перевожу книги по применения Elixir издательства The Pragmatic Bookshelf. Дошла очередь и до книги «Metaprogramming Elixir» Крис Маккорда (Chris McCord), которая занимает особое место в библиотеке по Elixir.
Цитата из аннотации этой книги:
«Эта книга представляет собой серию руководств по метапрограммированию, которые шаг за шагом приведут вас к мастерству метапрограммирования. Вы расширите Elixir мощными функциями и напишете более быстрые и удобные в обслуживании программы способами, не имеющими себе равных в других языках.»
Один из отзывов, извлеченный из предисловия в книге. Отзыв, конечно, положительный:
«Эта книга — именно то, что нужно молодому сообществу Elixir! Крис МакКорд изящно излагает метапрограммирование Elixir шаг за шагом, с практическими и удивительно поучительными примерами»
–Брюс Тейт @redrapids , президент RapidRed, LLC
Об авторе книги:
В Интернете можно найти биографическую информацию про Крис Маккорда, что он —создатель известного фреймфорка в Elixir Phoenix. И это всё. Но, судя по тому, что ему поручили написание одной из центральной книги по Elixir, Крис Маккорд является признанным гуру по Elixir. Кто такой я, и кто такой Крис Маккорд. Но всё-таки попробуем в меру своего понимания объективно оценить его книгу.
Хотя «Metaprogramming Elixir» является эталлонной книгой по макросам Elixir, но не является начальным учебником по макросам. Крис Маккорд не излагает материал методически последовательно и не опускается на уровень неофита, а пытается «поднять» его на технический уровень готовых небольших, но профессиональных проектов. Поэтому, как сказано в рецензии, «удивительно поучительные примеры» — это основной методический приём.
Крис Маккорд как мастер технично проводит читателя по вершинам знаний о макросах, но не очень заботится о безопасности прохода неофитов. Не каждый читатель сможет преодолеть предложенный траверс между вершинами. Я считаю, что читатель берет в руки учебник в первую очередь не из-за академического интереса, а с целью найти ответы на решение своих задач. Чтобы не быть голословным, в конце статьи я дам ссылку на мой взгляд доступный для неофитов учебник, в котором есть главы введения в макросы Elixir.
С другой стороны, по указанной причине в книге есть учебно–методические промашки, которые я назвал мастерскими казусами. В этой статье укажу на два обнаруженных казуса. Думаю, что их обнародование может быть не менее интересно для «учеников», чем демонстрация мастерства «учителя». По крайней мере, это может кумулятивно сэкономить время у коллег на распутывание загадок от Крис Маккорда.
Конечно, это не умаляет достоинства книги Крис Маккорда «Metaprogramming Elixir».
После представления и характеристики Криса Маккорда и его книги далее для краткости буду упоминать его по имени Крис.
В книге Криса в разделе «Adding a while Loop to Elixir» на стр. 22 предлагается реализовать макрос цикла while, отсутствующий в Elixir.
Целевая форма реализации:
while Process.alive?(pid) do
send pid, {self, :ping}
receive do
{^pid, :pong} -> IO.puts "Got pong"
after 2000 -> break
end
end
Алгоритм написан как прототип в понятном псевдокоде (Теперь это принято называть оператором DSL предметной области), который предстоит преобразовать в макрос.
Работа в алгоритме ведется с типовым в Elixir процессом. Процессу с идетификатором pid, пока он живой, посылается (send) пинг, который принимается (receive) и на консоль выводится сообщение о приёме пинга.
Здесь Крис явно торопился и допустил опечатку, написав понг. Везде должен стоять пинг, иначе сообщение вообще не будет распознано и принято.
Завершает алгоритм так называемый псевдо шаблон сообщения after, который срабатывает как тригер через 2000 млс простоя и прерывает процесс.
Далее привожу полученную Крисом окончательную реализацию макроса while:
defmodule Loop do
defmacro while(expression, do: block) do
quote do
try do
for _ <- Stream.cycle([:ok]) do
if unquote(expression) do
unquote(block)
else
Loop.break
end
end
catch
:break -> :ok
end
end
end
def break, do: throw :break
end
Не вдаваясь в технику макросов (кому нужно копать глубже, дам переводы двух глав учебника по Elixir, но не от Криса), обратим внимание в строке 5 на итератор for для порождения бесконечного потока. Как комментирует это сам Крис:
«главная проблема заключается в том, что в Elixir нет встроенного способа бесконечного цикла. Как же нам справиться с повторяющимся циклом без такой возможности? Мы схитрим. Мы можем подойти к делу творчески, используя бесконечный поток с помощью итератора for»
Тут я встрепенулся:
— Как нет. Испокон веков в функциональных языках принято использовать концевые рекурсии в качестве циклов. Этот механизм используется для организации работы процессов…
Сказано сделано. Выполнил реинжиниринг макроса while на базе конечной рекурсии. Работает в точности как Криса! Кто хочет, может повторить работу самостоятельно, т.к. статья всё–таки рассчитана на пробуждение интереса к технологии макросов в частности и к Elixir в целом.
Ещё один важный момент в модуле Loop — введена функция API def break для внешнего принудительного прерывания цикла. Конечно, это важно.
Пришло время испытать полученный макрос. Конечный пользовательский код для запуска цикла while:
iex>
pid = spawn fn ->
while do
receive do
:stop ->
IO.puts "Stopping..."
break
message ->
IO.puts "Got #{inspect message}"
end
end
end
Небольшой комментарий к приведенному коду. Процесс порождается оператором spawn, которому передаётся анонимная функция fn. Аргумент true задаёт бесконечный цикл while.
Далее следует проверка работоспособности макроса в составе порожденного процесса:
iex> send pid, :hello
Got :hello
:hello
iex> send pid, :ping
Got :ping
:ping
iex> send pid, :stop Stopping...
:stop
iex> Process.alive? pid
false
Процесс остановлен через функцию API. Проведем свою дополнительную проверку:
iex> send pid, :hello
:hello
Ответ : hello возвращает «холостой» вызов оператора send. Все работает безупречно.
Так, где же обещанный казус? А вот он: в начальной цели прототипа фигурировал pid в качестве аргумента макроса, а в реализации его нет — он вынесен на пользовательский уровень. Это вызвало у меня когнитивный разрыв между целеполаганием и полученным результатом.
Это потом я догадался, что встроить pid в тело макроса не удастся, так как он является результатом порождения процесса и является внешней ипостасью самого процесса. Не убедил? Тогда приведу такой аргумент: после «убития» процесса через send pid, : stop, у нас исчезает само тело процесса, которое может обработать условие прототипа while Process.alive?(pid).
Но этот цикл на самом деле существует в ручном виде и Крис демонстрирует это уже в интерактивной оболочке iex, только забывает это пояснить.
Неудобно сказать, но я нашёл ещё один серьёзный казус (?!) у мэтра. Небольшое отступление перед пояснением моего расследования: перед компиляции кода макросы манипулируют абстрактным синтаксическим деревом (AST) программы.
В самом начале книги Крис на стр. 9. книги пишет черным по белому:
«All Elixir code is represented as a series of three-element tuples with the following format:
• The first element is an atom denoting the function call, or another tuple, representing a nested node in the AST.
• The second element represents metadata about the expression.
• The third element is a list of arguments for the function call.»
Автор строит рекурсивную модель AST через ссылку в первый элемент кортежа (атом, обозначающий вызов функции), чего быть не может по определению; рекурсивная ссылка реализуется в третьем элементе, где хранится список аргументов.
Нарисуйте любое синтаксическое дерево, более сложное, чем три этажа, и вы наглядно убедитесь: дерево растёт сверху вниз через узлы аргументов. ниже приведена иллюстрация из книги «Metaprogramming Elixir».
quote do — макрос для преобразования выражения Elixir в AST, т.е. представление AST на лексемном уровне Elixir.
Слева графическое представление AST, справа — его внутреннее представление в виде кортежа. Если эти азы метапрограммирования показалось непонятным, то пока не придавайте этому значение. Переходите сразу к выводом статьи.
Я не поленился и написал об ошибке на сайте Errata книги, но ответа за два года не дождался. Потом я обнаружил, что Крис вообще не отвечает на все сообщения в Errata. Думаю, что он очень загружен.
Я был бы очень рад ошибиться в своём мнении, ведь мэтры никогда не ошибаются. Поэтому буду благодарен, если меня кто-нибудь поправят.
А теперь небольшой бонус, для тех, кто добрался до этого места статьи, — перевод 2–ой главы книги «Metaprogramming Elixir» и перевод 3–глав учебника для начинающих программистов на Elixir «Programming Elixir. Functional |> Concurrent |> Pragmatic |> Fun The» Дейва Томаса. Стиль изложения близок к стилю известного писателя учебников по программированию Г.Шилтд.
К сожалению, я не нашёл способ разместить ссылки на файлы pdf в статье, поэтому, кому надо, пишите в личку.
Выводы.
В завершении приведу рекомендацию от Дейва Томаса:
«макросы могут усложнить понимание кода, потому что вы, по сути, переписываете часть языка. По этой причине никогда не используйте макрос там, где можно было бы использовать функцию.»
Тогда возникает справедливый вопрос об объективности критерия целесообразности применения макросов Elixir.
Возьму на себя смелость и предложу читателям один такой критерий — применяйте макросы Elixir там, где необходимо производить вычисления в процессе трансляции. Если читатели будут к настоящей статье благосклонны, то напишу об этом в следующий раз.
Удачи вам на вершинах Elixir. Надеюсь, что статья принесла вам пользу.
Литература:
1. Chris McCord, Metaprogramming Elixir. Write Less Code, Get More Done (and Have Fun!). The Pragmatic Bookshelf, 2015
2. Dave Thomas, Programming Elixir. Functional |> Concurrent |> Pragmatic |> Fun The Pragmatic Bookshelf, 2014. (более свежая вторая редакция выходила 2018, но переводчику недоступна)