14.5.5 Сколько раз расширяется макрос?

Иногда проблемы возникают из-за того, что вызов макроса расширяется каждый раз, когда он вычисляется во встроенной функции, но расширяется только один раз (во время компиляции) для скомпилированной функции. Если в определении макроса есть побочные эффекты, они будут работать по-разному в зависимости от того, сколько раз макрос расширяется.

Поэтому следует избегать побочных эффектов при вычислении расширения макроса, если вы не уверены в своих действиях.

Один особый вид побочных эффектов нельзя избежать: создание объектов Lisp. Почти все расширения макросов включают сконструированные списки; это и есть основная цель большинства макросов. Обычно это безопасно; есть только один случай, когда следует быть осторожным: когда объект, который вы конструируете, является частью зацитированной константы в расширении макроса.

Если макрос расширяется только один раз, во время компиляции, то объект конструируется только один раз, во время компиляции. Но во время интерпретируемого выполнения макрос расширяется каждый раз, когда выполняется вызов макроса, и это означает, что каждый раз создается новый объект.

В большинстве чистого кода Lisp эта разница не будет иметь значения. Она может иметь значение только в том случае, если вы выполняете побочные эффекты над объектами, созданными определением макроса. Таким образом, чтобы избежать проблем, избегайте побочных эффектов над объектами, созданными определениями макросов. Вот пример того, как подобные побочные эффекты могут создать для вас проблемы:

(defmacro empty-object ()
  (list 'quote (cons nil nil)))

(defun initialize (condition)
  (let ((object (empty-object)))
    (if condition
        (setcar object condition))
    object))

Если initialize интерпретируется, каждый раз, когда вызывается initialize, создается новый список (nil). Таким образом, никакие побочные эффекты не сохраняются между вызовами. Если initialize компилируется, то макрос empty-object расширяется во время компиляции, создавая одну константу (nil), которая повторно используется и изменяется каждый раз, когда вызывается initialize.

Один из способов избежать патологических случаев, подобных этому, — рассматривать empty-object как своего рода константу, а не как конструкцию выделения памяти. Вы бы не использовали setcar на константе вроде '(nil), поэтому, естественно, вы не будете использовать его и на (empty-object).