Квазицитирование: 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, где этот инструмент раскроется полностью; но уже сейчас вы можете писать читаемые макросы, видя их результат как на ладони.

Проверьте себя
1. Чем отличается ,items от ,@items внутри backquote, если items = (a b c)?
AРазличий нет
B,items вставляет список (a b c) как один элемент; ,@items вклеивает a, b, c по отдельности
C,items вклеивает элементы, ,@items вставляет список целиком
D,@items работает только с числами
2. Какой инструмент покажет, во что разворачивается макрос, для отладки?
A(eval form)
B(macroexpand-1 'form) — показывает результат одного шага расширения
C(funcall form)
D(quote form)