Макросы: код как данные и defmacro

Почему макросы — главная суперсила Lisp: код как данные, фазы компиляции и defmacro как способ расширять сам язык.

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

Зачем это: однородность кода и данных (гомоиконность)

Чтобы понять макросы, надо принять одну идею, на которой стоит весь Lisp: код и данные — это одно и то же. Программа на Lisp записывается в виде списков: (+ 1 2) — это список из трёх элементов: символа + и чисел 1, 2. То же самое можно создать как данные: (list '+ 1 2). Это свойство называется гомоиконностью: синтаксис языка совпадает с его базовой структурой данных (списком). Следствие колоссально: программа может строить и преобразовывать другие программы теми же средствами, что и любые списки — через cons, list, mapcar. Макрос — это и есть программа, которая пишет программу.

В языках без гомоиконности «метапрограммирование» — это либо текстовые препроцессоры (хрупкие, как #define в C), либо внешние генераторы кода, либо ограниченная рефлексия. В Lisp расширение языка — штатная, безопасная и структурная операция: вы работаете не со строками, а с деревьями выражений (AST), которые язык и так использует. Поэтому в Lisp можно добавить новую управляющую конструкцию, новый вид объявлений, целый встроенный язык — не меняя компилятор. Большинство «встроенных» форм (when, cond, dotimes, loop) — сами макросы.

Сравнение с препроцессором C проясняет, почему «структурность» так важна. #define в C работает с текстом: он подставляет символы, ничего не зная о синтаксисе, поэтому печально известен ошибками вроде #define SQR(x) x*x, где SQR(a+b) разворачивается в a+b*a+b — текстовая подстановка не уважает структуру выражения. Lisp-макрос работает не с текстом, а с уже разобранным деревом: (a+b) для него — это список, цельный узел, который нельзя «случайно разорвать» приоритетом операций. Поэтому класс ошибок «макрос сломал структуру выражения» в Lisp просто отсутствует. Остаются другие ловушки (захват имён, двойное вычисление — мы их разберём), но они тоньше и решаемы дисциплиной, а не фундаментальны. Эта разница — «текст против дерева» — и есть причина, по которой Lisp-макросы считают настоящим метапрограммированием, а C-макросы — лишь текстовым трюком.

Историческое замечание, объясняющее, почему это «суперсила». Многие возможности, которые сегодня встроены в мейнстримные языки, в мире Lisp десятилетиями добавлялись пользователями через макросы, без изменения компилятора: объектные системы, обработка исключений, генераторы, сопоставление с образцом, асинхронность. Когда в другом языке нужна новая конструкция, сообщество ждёт следующей версии и комитета по стандартизации; в Lisp её пишут за вечер как библиотеку-макрос. Эта способность «догонять и опережать» эволюцию языка силами обычного программиста — то, что Пол Грэм называл секретным оружием Lisp. Поэтому изучение макросов — это не изучение «ещё одной фичи», а изучение того, как в Lisp вообще появляются фичи.

Макрос против функции: главная разница

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

;; Функция: видит значения. (square (+ 1 2)) -> square получает 3
(defun square (x) (* x x))

;; Макрос: видит исходную форму. (swap a b) -> макрос получает
;; символы A и B (не их значения) и СТРОИТ новый код.
(defmacro swap (a b)
  (list 'rotatef a b))

;; Зачем макрос? Функция не смогла бы поменять переменные местами:
;; она получила бы значения, а не сами места хранения.

Ключевое: swap обязан быть макросом, потому что ему нужны имена переменных (места), а не их значения. Функция вычислила бы аргументы и потеряла бы доступ к самим переменным. Это первый критерий «когда нужен макрос»: когда вы хотите управлять вычислением аргументов — отложить его, повторить, обернуть, или работать с самими формами, а не значениями.

defmacro: анатомия определения

Макрос определяется через defmacro. Синтаксически он похож на defun: имя, список параметров, тело. Но семантика иная — тело выполняется во время компиляции (на этапе «макрорасширения»), а его результат — это код. Тело макроса должно вернуть форму (список), которая и подставится.

;; Макрос my-unless: «выполни тело, если условие ложно»
(defmacro my-unless (condition &body body)
  (list 'if condition nil (cons 'progn body)))

;; Использование:
(my-unless (> 3 5)
  (format t "три не больше пяти~%"))

;; Во что развернётся:
;; (IF (> 3 5) NIL (PROGN (FORMAT T "три не больше пяти~%")))

Параметр &body — это специальный лямбда-список, почти синоним &rest: он собирает «остаток» форм в список, но сигнализирует читателю и средам разработки, что это «тело» (его принято отбивать отступом). Внутри макроса body — это список форм, который мы оборачиваем в (progn ...). Обратите внимание: мы строим код вручную через list и cons. Это работает, но громоздко — в следующем уроке мы заменим это на квазицитирование, которое делает построение кода наглядным.

Чтобы закрепить разницу «функция видит значения / макрос видит формы», сравните, что именно получает каждый. Если определить функцию и макрос, печатающие свой аргумент, разница станет очевидной: функции достаётся уже вычисленное число, а макросу — исходная форма-список, не тронутая вычислением.

(defun  f-show (x) (format t "функция видит: ~s~%" x))
(defmacro m-show (x) (format t "макрос видит: ~s~%" x) nil)

(f-show (+ 2 3))    ; печатает: функция видит: 5      (значение)
(m-show (+ 2 3))    ; печатает (во время компиляции):
                    ;          макрос видит: (+ 2 3)   (исходная форма!)

Вот она, граница двух миров в одном примере: f-show получила 5, потому что аргумент функции вычисляется до вызова; m-show получила список (+ 2 3) — сам исходный код, как данные, ещё до всякого вычисления. Именно эта разница и даёт макросам власть, недоступную функциям: возможность смотреть на структуру кода и переписывать её.

Две фазы: время компиляции и время выполнения

Самое важное для правильной модели — различать две фазы. Когда компилятор встречает (my-unless test body...), он:

  1. Фаза макрорасширения (compile time): вызывает функцию-макрос my-unless, передавая ей невычисленные формы test и body. Макрос возвращает новый код — например, (if test nil (progn body...)).
  2. Фаза компиляции/выполнения (run time): возвращённый код подставляется на место вызова и компилируется/выполняется уже как обычная программа.

Эта двухфазность объясняет всё поведение макросов. Например, аргументы макроса не вычисляются перед передачей — макрос сам решает, вычислять ли их и когда (вставив в результат). Поэтому макрос может выполнить аргумент дважды, ноль раз или в особом контексте. И поэтому код в теле макроса (логика построения) исполняется до того, как программа «запустится» — он работает с формами, а не со значениями. Спутать эти фазы — корень почти всех ошибок с макросами.

Когда макрос, а когда функция

Сильное правило: по умолчанию пишите функцию; макрос — только когда функция не справится. Функции проще, отлаживаются легче, их можно передавать как значения. Макрос оправдан, когда нужно одно из:

  • Управлять вычислением: отложить (my-unless вычисляет тело только при условии), повторить, обернуть в контекст.
  • Работать с местами/именами: swap, incf, setf — им нужны «места», а не значения.
  • Создавать новый синтаксис/DSL: декларативные конструкции, удобный сахар, доменные языки.
  • Делать что-то в compile time: генерировать код, проверять во время компиляции.

Если ничего из этого не нужно — функция лучше. Классическая ошибка новичка — «макросы это круто, напишу всё макросами»: получается хрупкий, плохо отлаживаемый код. Опытный лисповец бережёт макросы для тех случаев, где они незаменимы.

Полезное практическое правило поверх этого: сначала напишите функцию, а макрос добавьте лишь как тонкую синтаксическую обёртку, если она вообще нужна. Очень часто оказывается, что 90% работы прекрасно делает функция (она несёт логику), а макрос требуется максимум чтобы избавить пользователя от написания lambda или цитирования. Такой «макрос-обёртка над функцией» сочетает лучшее: логику легко тестировать и отлаживать как обычную функцию, а синтаксис остаётся удобным. Этот приём — «механизм в функции, синтаксис в макросе» — мы подробно разберём в уроке про DSL; пока запомните его как противоядие от соблазна решать макросами то, что им не по адресу.

Как работает под капотом

Технически макрос — это обычная функция, привязанная к символу в «макро-ячейке» (macro function), отдельной от «функциональной ячейки». Компилятор, обходя код, для каждой формы (operator . args) проверяет: если operator — символ с макро-функцией, происходит макрорасширение: компилятор вызывает эту функцию с самой формой как аргументом и заменяет форму результатом. Затем процесс повторяется (результат может снова содержать макросы — расширение идёт, пока не останутся только функции и особые формы). Этот рекурсивный обход и есть «walk» по дереву кода. Поскольку макрорасширение происходит до генерации машинного кода, у макросов нет накладных расходов во время выполнения: к моменту запуска программы все макросы уже развёрнуты в обычные формы. Это отличает Lisp-макросы от, скажем, интерпретируемых преобразований: они «бесплатны» в рантайме.

Частые ошибки

  • Путать фазы. Тело макроса исполняется во время компиляции и работает с формами, а не значениями. Не пытайтесь «вычислить» аргумент в теле макроса напрямую — вы получите его исходный код.
  • Делать макросом то, что должно быть функцией. Если не нужно управлять вычислением, именами или синтаксисом — пишите функцию.
  • Возвращать из макроса значение вместо кода. Макрос должен вернуть форму (код), которая потом вычислится, а не готовое значение.
  • Забывать, что аргументы не вычислены. Если ваш макрос подставит аргумент в результат дважды — он и вычислится дважды (с побочными эффектами!). Это реальная ловушка, её решают через let+gensym (следующие уроки).
  • Не проверять разворот. Всегда смотрите, во что разворачивается макрос (через macroexpand-1) — это лучший способ отладки.

Итоги

  • Lisp гомоиконен: код — это списки, поэтому программы могут строить программы штатными средствами.
  • Макрос получает невычисленный исходный код аргументов и возвращает новый код, подставляемый на место вызова.
  • В отличие от функции, макрос управляет вычислением аргументов и может работать с именами/местами.
  • defmacro похож на defun, но его тело исполняется на этапе макрорасширения и возвращает форму.
  • Две фазы — макрорасширение (compile time) и выполнение (run time) — основа правильной модели.
  • Правило: по умолчанию функция; макрос — только для управления вычислением, имён, синтаксиса или работы в compile time.
Проверьте себя
1. Чем макрос принципиально отличается от функции?
AМакрос работает быстрее функции в рантайме
BМакрос получает невычисленный исходный код аргументов и возвращает новый код, а функция получает вычисленные значения
CФункция не может возвращать значение, а макрос может
DМакросы существуют только в Scheme, а функции — в Common Lisp
2. Почему swap (обмен значений двух переменных) обязан быть макросом, а не функцией?
AФункции в Lisp не умеют менять переменные
BМакросу нужны сами имена переменных (места хранения), а функция получила бы лишь их значения и потеряла доступ к местам
CЭто требование стандарта ANSI
DМакросы выполняются быстрее