Идиомы использования макросов в Лиспах

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

Соответствующий механизм здесь называется макросами. Макросы это функция, получающая на вход код структуры данных, и меняющая ее по своему усмотрению, и возвращающая видоизмененный код (тоже как структуру данных). После выполнения (раскрытия) макроса интерпретатор исполнит получившийся код:

(defmacro evaluate-left-only (left-form right-form)
    left-form)
(evaluate-left-only (print t) (print nil))
;;;; evaluating (print t)
;; => t

В примере мы ввели два макроса. Макрос (evaluate-left-only) передает, не меняя, интерпретатору свой левый аргумент (left-form), который и вычисляется, выводя t.

Другими словами, макросы - мощнейший инструмент, позволяющий программисту менять смысл (семантику) выражений языка по своему усмотрению. В сущности, богатые на макросы программы превращаются в специализированные мини-языки, затрудняющие чтение кода незнакомыми с кодом разработчиками.

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

Синтаксический сахар

Некоторые синтаксические конструкции можно выразить в основном языке, но не очень читаемо. if при отсутствии ветки else читается тяжело, поэтому удобней использовать макросы when/unless:

(when t
    (print "true"))
;; => "true"
(unless nil
    (print "false"))
;; => "false"

Здесь макросы это просто очень тонкая обертка над обычным if, но бывают и продвинутые примеры, когда на макросах строятся целые фреймворки, и где макросы сокращают объем необходимого кода.

Анафорические макросы

Знаменитый Пол Грэм в классической книге On Lisp популяризовал анафорические макросы:

(aif 10
    (print it))
;; => 10

Здесь вычисляется первый аргумент, и если он не является nil, то его значение становится переменной it ("оно") в теле условия. Вариантов этого приема множество, в том числе и в рамках других паттернов.

Мини-языки

Как известно, Common Lisp включил в себя вообще все, что может включать язык. Ярчайший пример - супермакрос loop:

(loop for var from 1 to 100
    collect var while (< var 5))
;; => (1 2 3 4 5)

Макрос loop в данном случае определен через полноценную грамматику и работает как небольшой компилятор.

Управление контекстом

Еще один интересный пример это запуск кода в определенном контекста. Традиционно названия таких макросов начинаются с префикса with-, и запускают код-аргумент с какими-то изменениями в среде выполнения. Вот, например, пример из Emacs Lisp:

(with-temp-file "testfile"
    (insert "test1")
    (insert "test2")
    (insert "test3"))

Здесь макрос откроет файл testfile, запишет туда строку "test1test2test3" и закроет файл. Аналогичным образом можно построить управление ресурсами в стиле RAII в C++ или context managers из Python.

Мораль

Макросы надо любить, и макросов надо бояться.

Приведенные случаи - самые чистые из возможных идиом, использование которых можно только приветствовать. Практически же используются и довольно сложные обходы дерева кода в макросах, и не очень красивые хаки, и вводятся новые языковые возможности. И все это может усложнить ваш код.

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

В общем, Viva la Lisp!