Макросы и метапрограммирование
Добираемся до главной суперсилы Lisp: пишем код, который пишет код.
Макрос — конструкция, которая получает невыполненный код как данные, преобразует его и возвращает новый код, подставляемый на место вызова на этапе компиляции.
Чем макрос отличается от функции
Функция получает вычисленные аргументы. Макрос получает аргументы как код (структуры данных) и сам решает, что с ним делать — выполнять, переставлять, оборачивать. Благодаря гомоиконности (помните раздел «код как данные»?) код — это списки, и макрос свободно их преобразует.
; встроенный макрос when - сахар над if без ветки else
(when true
(println "шаг 1")
(println "шаг 2"))
; разворачивается в (if true (do (println ...) (println ...)))Вывод:
шаг 1 шаг 2
Пишем свой макрос
Объявляют макрос через defmacro. Сделаем макрос если-не, который выполняет тело, только если условие ложно. Он строит код с if и возвращает его:
(defmacro если-не [условие тело]
(list 'if условие nil тело))
(если-не false (println "условие ложно — выполнилось"))Вывод:
условие ложно — выполнилось
Макрос вернул структуру (if false nil (println ...)), и компилятор подставил её на место вызова. Само тело (println ...) не выполнялось, пока макрос его не «решил» выполнить — в этом ключевое отличие от функции.
Шаблоны: цитирование с подстановкой
Строить код руками через list утомительно. Для шаблонов есть синтаксическое цитирование (обратная кавычка) и подстановка (тильда), но идея та же — описать форму результата:
; псевдо-форма шаблона макроса (для понимания идеи):
; `(if ~условие nil ~тело)
; обратная кавычка фиксирует структуру, тильда вставляет
; значение аргумента в нужное местоЗачем это суперсила
Макросы позволяют расширять сам язык. В большинстве языков конструкции вроде if, for, try встроены и неприкосновенны — добавить свою нельзя. В Lisp when, and, or, трединг-макросы -> и ->> — всё это обычные макросы, написанные на самом языке. Вы можете создавать собственные конструкции под свою предметную область (DSL).
Как работает под капотом
Макроразворачивание происходит на этапе компиляции, до выполнения. Компилятор, встретив вызов макроса, передаёт ему аргументы как невычисленный код, получает обратно новый код и компилирует уже его. Проверить разворачивание помогает macroexpand. Поскольку всё происходит при компиляции, в рантайме макрос не стоит ничего — это чистое преобразование исходника.
Частые ошибки
- Писать макрос, где хватит функции. Макросы нужны, только когда требуется управлять вычислением аргументов или генерировать код. Иначе берите функцию.
- Забыть, что аргументы не вычислены. В макрос приходит код; если нужно его значение, надо встроить вычисление в результат.
- Гигиена имён. Имена, введённые макросом, могут случайно пересечься с пользовательскими; в реальных макросах для этого применяют автогенерацию имён.
Итоги
- Макрос получает код как данные и возвращает новый код на этапе компиляции.
- Объявляется через
defmacro; в отличие от функции, аргументы не вычисляются. - Макросы позволяют расширять язык и строить DSL — это суперсила Lisp.
- Разворачивание происходит при компиляции; проверить его можно через
macroexpand.