Квазицитирование: backquote, запятая, ,@ и macroexpand
Квазицитирование: backquote, запятая и запятая-собака — как строить код наглядно, словно по шаблону, и как разворачивать макросы для отладки.
Квазицитирование (backquote) — это «шаблон кода»: всё внутри обратной кавычки берётся буквально, кроме помеченных запятой мест, куда подставляется вычисленное значение.
Зачем это: от ручной сборки кода к шаблону
В прошлом уроке мы строили код макроса через list и cons: (list 'if condition nil (cons 'progn body)). Это работает, но плохо читается — структура результата теряется в вызовах list. Квазицитирование решает это радикально: вы пишете как выглядит результат, оставляя «дырки» для подстановки. Это превращает построение кода в заполнение шаблона — наглядно и близко к итоговой форме. Для макросов это незаменимо: 99% макросов используют backquote.
Аналогия, которая делает идею мгновенно понятной: backquote — это «строковая интерполяция, но для кода». В современных языках есть шаблоны строк вроде "Привет, {name}!", где почти всё — литеральный текст, а в фигурных скобках подставляется значение. Backquote — то же самое, но результат не строка, а структура кода: `(привет ,name) — почти всё буквально (символ привет), а после запятой подставляется значение. Разница принципиальна: интерполяция строит текст, который потом надо парсить; backquote сразу строит дерево-форму, готовую к вычислению. Но интуиция «шаблон с подставляемыми дырками» переносится один в один и сильно снижает порог входа.
Три знака: ` , ,@
Обычная одинарная кавычка 'x (это quote) защищает выражение от вычисления целиком: '(a b c) — это список из трёх символов. Backquote `x делает почти то же — берёт буквально — но позволяет «прорезать дырки» через запятую:
- Backquote
`(обратная кавычка) — начинает шаблон. Всё внутри буквально, как приquote. - Запятая
,(unquote) — «вычисли это и подставь значение сюда». Локальный выход из цитирования. - Запятая-собака
,@(unquote-splicing) — «вычисли это (должен получиться список) и вклей его элементы сюда», убрав внешние скобки.
(let ((x 10)
(items '(a b c)))
;; backquote с подстановками:
`(value is ,x and list is ,items))
;; => (VALUE IS 10 AND LIST IS (A B C))
;; разница , и ,@:
(let ((items '(a b c)))
(list `(start ,items end) ; items как ОДИН элемент-список
`(start ,@items end))) ; элементы items ВКЛЕЕНЫ
;; => ((START (A B C) END)
;; (START A B C END))
Разница между , и ,@ — ключевая и частый источник путаницы. ,items вставляет сам список (a b c) как один элемент. ,@items «расшивает» его, вставляя a b c по отдельности, словно их написали прямо в шаблоне. ,@ применяется, когда у вас есть список форм, которые надо «влить» в окружающую форму — например, тело макроса.
Переписываем макросы красиво
Теперь перепишем my-unless из прошлого урока через backquote — сравните читаемость:
;; Было (ручная сборка):
(defmacro my-unless (condition &body body)
(list 'if condition nil (cons 'progn body)))
;; Стало (квазицитирование) — видно структуру результата:
(defmacro my-unless (condition &body body)
`(if ,condition
nil
(progn ,@body)))
;; Макрос «выполни тело n раз с печатью номера»
(defmacro repeat-loud (n &body body)
`(dotimes (i ,n)
(format t "итерация ~a~%" i)
,@body))
В новой версии my-unless сразу видно: получится форма (if ... nil (progn ...)). ,condition подставит переданное условие, ,@body вклеит все формы тела в progn. Это и есть идиоматичный Lisp: шаблон кода с дырками. Заметьте, как ,@body именно «вливает» список форм — если бы мы написали ,body, в progn попал бы один элемент-список, и код сломался бы.
Это различие настолько частое, что стоит сформулировать его как мнемонику. Спросите себя про каждую дырку в шаблоне: «здесь будет одна вещь или список вещей, которые надо разложить?». Если одна (значение переменной, результат вычисления, единичная форма) — обычная запятая. Если несколько, и они должны встать в окружающую форму как самостоятельные элементы (тело из нескольких форм, список аргументов, набор объявлений) — запятая-собака. Параметры макроса, собранные через &body или &rest, — это всегда списки, и почти всегда их подставляют через ,@. А одиночные параметры — через ,. Держа этот вопрос в голове, вы перестанете путать два знака, а это, без преувеличения, самая частая ошибка при первом знакомстве с backquote.
macroexpand: смотрим, во что развернулось
Главный инструмент отладки макросов — посмотреть на результат расширения. Для этого есть macroexpand-1 (один шаг расширения) и macroexpand (расширять, пока на верхнем уровне остаются макросы). Они принимают цитированную форму и возвращают развёрнутый код.
(macroexpand-1 '(my-unless (> 3 5)
(format t "hi~%")))
;; => (IF (> 3 5) NIL (PROGN (FORMAT T "hi~%")))
;; T (второе значение T = «это был макрос»)
(macroexpand-1 '(repeat-loud 2 (do-thing)))
;; => (DOTIMES (I 2)
;; (FORMAT T "итерация ~a~%" I)
;; (DO-THING))
Привычка всегда разворачивать новый макрос перед использованием экономит часы. Если разворот выглядит не так, как вы ожидали, — ошибка в шаблоне, и видно где. macroexpand-1 делает ровно один шаг (удобно, когда макрос разворачивается в другой макрос — видно каждый слой), macroexpand разворачивает до конца верхнего уровня. Многие IDE (SLIME/Sly) показывают разворот по горячей клавише прямо в редакторе.
Почему разворот так важен именно для отладки? Потому что ошибка в макросе проявляется дважды и на разных этапах. Бывает ошибка в самом шаблоне (вы построили не тот код) — её видно в разворте. А бывает ошибка в сгенерированном коде (шаблон верен, но порождённый код некорректен) — её видно уже при выполнении развёрнутого кода. Без разворота эти два уровня сливаются, и сообщение об ошибке указывает на загадочное место «внутри макроса». Развернув руками, вы разделяете вопросы: «правильный ли код я генерирую?» (смотрим разворот) и «правильно ли работает этот код?» (выполняем разворот отдельно). Это дисциплина, которая отличает уверенную работу с макросами от гадания, и её стоит выработать с первого же написанного макроса.
Ещё одна частая практическая нужда — подставить в шаблон не значение переменной, а результат вычисления, и сделать это на этапе расширения. Поскольку запятая означает «вычислить здесь», в неё можно положить любой код, включая обращения к параметрам макроса и вспомогательные функции. Это позволяет макросу выполнять часть работы заранее — например, развернуть список имён в список объявлений, или сгенерировать серию похожих форм циклом mapcar внутри запятой-собаки. Граница простая: всё в запятой исполняется во время расширения и работает с формами/значениями, доступными тогда; всё вне запятой попадает в результат буквально и исполнится позже, когда выполнится сгенерированный код. Чёткое разделение этих двух «времён» — суть мастерства в макросах.
Вложенность и тонкости
Backquote можно вкладывать, и запятые «относятся» к ближайшему охватывающему backquote — это нужно для макросов, генерирующих макросы, и встречается редко. На практике запомните частый случай: внутри одного backquote запятая означает «вычислить здесь и сейчас». Ещё полезная идиома — ,(expr) может содержать любое вычисление, не только переменную: `(sum is ,(+ a b)) подставит результат сложения. И комбинация: `(items ,@(mapcar #'process raw)) — обработать список и вклеить результаты. Это делает backquote мощным шаблонизатором кода.
(let ((a 2) (b 3) (names '("x" "y")))
`(result ,(* a b) :names ,@(mapcar #'string-upcase names)))
;; => (RESULT 6 :NAMES "X" "Y")
Scheme: quasiquote те же идеи
В Scheme квазицитирование называется так же по смыслу, но знаки можно писать словами: backquote — quasiquote (или `), unquote — unquote (или ,), splicing — unquote-splicing (или ,@). Семантика идентична Common Lisp. Это один из участков, где диалекты совпадают почти буквально, потому что идея «шаблон кода с дырками» универсальна для Lisp.
;; Scheme:
(let ((x 10) (items '(a b c)))
`(value ,x list ,@items))
;; => (value 10 list a b c)
Как работает под капотом
Backquote — это читательский макрос (reader macro): он обрабатывается на этапе чтения исходника, ещё до компиляции. Reader превращает `(a ,b ,@c) в эквивалентный код построения списка — примерно в (list* 'a b c) или похожую комбинацию cons/append/list (точная развёртка зависит от реализации, стандарт оставляет её на усмотрение). То есть backquote — это просто удобная запись для той самой ручной сборки через list/cons, которую мы делали раньше; reader пишет её за вас. Поэтому в результате нет никакой «магии шаблонов» в рантайме: к моменту выполнения это обычный код, конструирующий список. Запятая помечает места, которые reader оставит «живыми» (вычисляемыми), а всё прочее закавычит. Поэтому, кстати, нельзя использовать запятую вне backquote — reader просто не будет знать, относительно какого шаблона её трактовать, и сообщит об ошибке чтения. Запятая существует только «внутри» обратной кавычки и всегда привязана к ближайшему охватывающему её backquote.
Частые ошибки
- Путать
,и,@.,вставляет значение как один элемент;,@вклеивает элементы списка по отдельности. Для «тела» макроса почти всегда нужен,@. - Забыть запятую. Без запятой выражение попадёт в код буквально (как символ), а не его значение.
`(x ,n)подставит значениеn, а`(x n)— символN. - Лишняя запятая снаружи backquote. Запятая имеет смысл только внутри backquote; вне его — ошибка чтения.
- Не разворачивать макрос. Всегда проверяйте
macroexpand-1: большинство багов шаблона видны сразу. - Ждать «список из
,@nil».,@вклеивает элементы; если подставляемое —nil(пустой список), не вклеится ничего, и это нормально.
Итоги
- Backquote
`— шаблон кода: всё буквально, кроме помеченных запятой мест. ,подставляет вычисленное значение как один элемент;,@вклеивает элементы списка по отдельности.- Для тела макроса используют
,@body— «влить» все формы тела в результат. macroexpand-1/macroexpandпоказывают разворот — главный инструмент отладки макросов.- В запятую можно вкладывать любое вычисление, включая
mapcarи арифметику. - Под капотом backquote — читательский макрос, превращающий шаблон в обычный код сборки списка.
Освоив backquote, вы получили рабочий язык построения кода, без которого макросы остаются громоздкими. Дальше — гигиена и собственный DSL, где этот инструмент раскроется полностью; но уже сейчас вы можете писать читаемые макросы, видя их результат как на ладони.