8.1 Определение макроса

Функция macro-function определяет, является ли данный символ именем макроса. Конструкция defmacro предоставляет удобный способ определить новый макрос.

[Функция] macro-function symbol &optional env

Первый аргумент должен быть символом. Если символ содержит определение функции, то есть определение макроса, локально созданное в окружении env с помощью macrolet или глобально созданное с помощью defmacro, тогда возвращается функция раскрытия (функция двух аргументов, первый форма макровызова и второй — окружение). Если символ не содержит определение функции, или это определение обычной функции или это оператор, но не макрос, тогда возвращается nil. Наилучший способ вызвать функцию раскрытия использовать macroexpand или macroexpand-1.

Возможно такое, что и macro-function, и special-operator-p обе возвращают истину для одного заданного символа. Так происходит, потому что реализация может для увеличения производительности реализовывать любой макрос, как специальную форму. С другой стороны, определения макросов должны быть доступны для использования программами, которые понимают только стандартные операторы, перечисленные в таблице 5.1.

setf может быть использована с macro-function для установки в символ глобального определения макроса:

(setf (macro-function symbol) fn)

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

Смотрите также compiler-macro-function.


[Макрос] defmacro name lambda-list [[{declaration}* | doc-string]] {form}*

defmacro является макросом определяющим макросы, которые преобразуют формы макровызовов. defmacro имеет почти такой же синтаксис, как и defun: name является символом, для которого создаётся макрос, lambda-list похож на список параметров лямбда-выражения и form содержит тело функции раскрытия. Конструкция defmacro устанавливает функцию раскрытия, как глобальное определение макроса для символа name. FIXME

Форма обычно используется на верхнем уровне, но её также можно использовать на неверхнем уровне. Таким образом, defmacro должна определять функцию раскрытия макроса в некотором лексическом, а не глобальном окружении.

Тело функции раскрытия, определяемое с помощью defmacro, неявно оборачивается в конструкцию block, имя которой совпадает с именем name, определяемого макроса. Таким образом для выхода из функции может использоваться return-from.

Форма defmacro возвращает в качестве значение name.

Если мы рассмотрим макровызов, как список содержащий имя функции и некоторые формы аргументов, то механизм заключается в передаче в функцию apply этой функции и списка её (невычисленных) аргументов. Спецификаторы параметров обрабатываются также, как и для любого лямбда-выражения. В качестве параметров используются формы аргументов макровызова. Затем вычисляются формы тела, как неявный progn. Значение последней формы возвращается, как раскрытие макровызова.

Если указана необязательная строка документации doc-string, тогда она присоединяется к символу name, как строка документации типа function. Смотрите documentation. Если в определении макроса представлена только строка документации, и после неё нет ни одной формы, ни деклараций, ни просто для тела, то данная строка сама становиться формой, и тело считается состоящим из одной формы — этой строки.

Следующие три дополнительных маркера доступны в определении лямбда-списка.

Смотрите lambda-list-keywords.

X3J13 voted in March 1989 to specify that macro environment objects received with the &environment argument of a macro function have only dynamic extent. The consequences are undefined if such objects are referred to outside the dynamic extent of that particular invocation of the macro function. This allows implementations to use somewhat more efficient techniques for representing environment objects.

X3J13 voted in March 1989 to clarify the permitted uses of &body, &whole, and &environment:

defmacro, в отличие от любых других конструкций Common Lisp’а, имеющих лямбда-списки в своём синтаксисе, предоставляет дополнительную функциональность известную как деструктуризация.

Смотрите destructuring-bind, которая отдельно предоставляет эту функциональность.

В любом месте, где могло бы стоять имя параметра, и где по синтаксису не ожидается использование списка (как описано в разделе 5.2.2), можно использовать ещё один встроенный лямбда-список. Когда использован такой приём, при передаче форм аргументов для встроенного лямбда-списка, необходимо обернуть эти формы в отдельный список. В качестве примера, определение макроса для dolist можно записать в таком стиле:

(defmacro dolist ((var listform &optional resultform)
                  &rest body)
  ...)

Ниже будет больше примеров использования встраиваемых лямбда-списков в defmacro.

Следующим правилом деструктуризации является то, что defmacro позволяет любому лямбда-списку (верхнего уровня или встроенному) быть dotted, заканчивающимся именем параметра. Такая ситуация обрабатывается так, как будто имя параметра в конце списка, стоит после неявного маркера &rest. Например, уже показанное определение dolist может быть записано так:

(defmacro dolist ((var listform &optional resultform)
                  . body)
  ...)

Если компилятор встречает defmacro, он добавляет новый макрос в окружение компиляции, и также функция раскрытия добавляется в выходной файл. Таким образом новый макрос будет более быстрым во время выполнения. Если необходимо избежать такого механизма, можно использовать defmacro внутри конструкции eval-when.

defmacro может также использоваться для переопределения макроса (например, для установки корректной версии определения вместо некорректной), или для переопределения функции в макрос. Переопределение оператора (смотрите таблицу 5.1) не допускается. Смотрите macrolet, которая устанавливает определение макроса в замкнутом лексическом области видимости.

Допустим, для примера, что необходимо реализовать условную конструкцию аналогичную Fortran’овскому арифметическому выражению IF. (Это конечно требует определённого расширения воображения и приостановки неверия.) Конструкция должна принимать четыре формы: test-value, neg-form, zero-form и pos-form. В зависимости от того, является ли test-form отрицательным, нулём или положительным числом, для выполнения будет выбрана одна из трёх последних форм. С использованием defmacro, определение этой конструкции может выглядеть так:

(defmacro arithmetic-if (test neg-form zero-form pos-form)
  (let ((var (gensym)))
    ‘(let ((,var ,test))
       (cond ((< ,var 0) ,neg-form)
             ((= ,var 0) ,zero-form)
             (t ,pos-form)))))

Необходимо отметить, что в данном определении используется функциональность обратной кавычки (смотрите раздел 22.1.3). Также необходимо заметить, что используется gensym для создания нового имени переменной. Это необходимо для избежания конфликтов с другими переменными, которые могут использоваться в формах neg-form, zero-form или pos-form.

Если форма выполняется интерпретатором, то определение функции для символа arithmetic-if будет является макросом, с которым ассоциирована функция двух аргументом эквивалентная данной

(lambda (calling-form environment)
  (declare (ignore environment))
  (let ((var (gensym)))
    (list ’let
          (list (list ’var (cadr calling-form)))
          (list ’cond
                (list (list ’< var ’0) (caddr calling-form))
                (list (list ’= var ’0) (cadddr calling-form))
                (list ’t (fifth calling-form))))))

Лямбда-выражение является результатом выполнения декларации defmacro. Вызов list является (гипотетически) результатом макросимвола обратной кавычки () и связанной с ним запятой. Конкретная функция раскрытия макроса может зависеть от реализации, например, она также может содержать проверку на корректность входных аргументов в макровызове.

Теперь, если eval встретит

(arithmetic-if (- x 4.0)
               (- x)
               (error "Strange zero")
               x)

то раскроет эту форму в

(let ((g407 (- x 4.0)))
  (cond ((< g407 0) (- x))
        ((= g407 0) (error "Strange zero"))
        (t x)))

и eval выполнит полученную форму. (Сейчас должно быть понятно, что функциональность обратной кавычки очень полезна для написания макросов. Она используется для построения шаблона, возвращаемой формы, с константными частями и частями для выполнения. Шаблон представляет собой «картину» кода, с местами для заполнения выделенными запятыми.)

Для улучшения примера мы можем сделать так, чтобы pos-form и zero-form могли быть заменены на nil в результате раскрытия макроса. Таким же образом действует и if опуская ветку else в случае истинности условия.

(defmacro arithmetic-if (test neg-form
                         &optional zero-form pos-form)
  (let ((var (gensym)))
    ‘(let ((,var ,test))
       (cond ((< ,var 0) ,neg-form)
             ((= ,var 0) ,zero-form)
             (t ,pos-form)))))

Тогда можно записать

(arithmetic-if (- x 4.0) (print x))

и это раскроется в что-то вроде

(let ((g408 (- x 4.0)))
  (cond ((< g408 0) (print x))
        ((= g408 0) nil)
        (t nil)))

Результирующий код корректен, но некрасиво выглядит. Можно переписать определение макроса для генерации лучшего кода, когда pos-form и zero-form могут быть вообще опущены. Или же можно положиться на реализацию Common Lisp’а, которая возможно оптимизирует этот код.

Деструктуризация является очень мощной функциональностью, которая позволяет лямбда-списку в defmacro выразить сложную структуру макровызова. Если не использовались ключевые символы лямбда-списка, то он представляет собой просто список с некоторой степенью вложенности и параметрами в качестве листьев. Структура макровызова должна иметь такую же структуру списка. Например, рассмотрим следующее определение макроса:

(defmacro halibut ((mouth eye1 eye2)
                   ((fin1 length1) (fin2 length2))
                   tail)
  ...)

Теперь давайте рассмотрим макровызов:

(halibut (m (car eyes) (cdr eyes))
         ((f1 (count-scales f1)) (f2 (count-scales f2)))
         my-favorite-tail)

Все это приведёт к тому, что функция раскрытия получит следующие значения для её параметров:

ПараметрЗначение
mouth m
eye1 (car eyes)
eye2 (cdr eyes)
fin1 f1
length1 (count-scales f1)
fin2 f2
length2 (count-scales f2)
tail my-favorite-tail

Следующий макровызов ошибочный, так как аргумента для параметра length1 не представлено:

(halibut (m (car eyes) (cdr eyes))
         ((f1) (f2 (count-scales f2)))
         my-favorite-tail)

Следующий макровызов также ошибочный, так как на месте предполагаемого списка указан символ.

(halibut my-favorite-head
         ((f1 (count-scales f1)) (f2 (count-scales f2)))
         my-favorite-tail)

Тот факт, что значение переменной my-favorite-head может быть списком, не имеет здесь значения. В макровызове структура должна совпадать с лямбда-списком в определении.

Использование ключевых символов лямбда-списка предоставляет ещё большую гибкость. Например, предположим, что удобно будет в функции раскрытия обращаться к элементам списка, называемым cdmouth, eye1, and eye2, как к head. Можно записать так:

(defmacro halibut ((&whole head mouth eye1 eye2)
                   ((fin1 length1) (fin2 length2))
                   tail)

Теперь рассмотрим такой же, как раньше, корректный макровызов:

(halibut (m (car eyes) (cdr eyes))
         ((f1 (count-scales f1)) (f2 (count-scales f2)))
         my-favorite-tail)

Это приведёт к тому, что функции раскрытия получит те же значения для своих параметров, а также значение для параметра head:

ПараметрЗначение
head (m (car eyes) (cdr eyes))

Существует условие для деструктуризации, встроенный лямбда-список разрешён только на позиции, где синтаксис лямбда-списка предусматривает имя параметра, но не список. Это защищает от двусмысленности. Например, нельзя записать

(defmacro loser (x &optional (a b &rest c) &rest z)
  ...)

потому что синтаксис лямбда-списка не позволяет использовать списки после &optional. Список (a b &rest c) был бы интерпретирован как необязательный параметр a, у которого значение по умолчанию b, и supplied-p параметр с некорректным именем &rest, и дополнительным символом c, также некорректным. Было бы правильнее выразить это так:

(defmacro loser (x &optional ((a b &rest c)) &rest z)
  ...)

Дополнительные круглые скобки устраняют двусмысленность. Однако, такой макровызов, как (loser (car pool)) не предоставляет никакой формы аргумента для лямбда-списка (a b &rest c), значит значение по умолчанию для него будет nil. А так как nil является пустым списком, то этот макровызов ошибочен. Полностью корректное определение выглядит так:

(defmacro loser (x &optional ((a b &rest c) ’(nil nil)) &rest z)
  ...)

или так

(defmacro loser (x &optional ((&optional a b &rest c)) &rest z)
  ...)

Они слегка отличаются: первое определение требует, что если макровызов явно указывает a, тогда он должен указать явно и b, тогда как второе определение не содержит такого требования. Например,

(loser (car pool) ((+ x 1)))

будет корректным макровызовом для второго определения, но не для первого.