Вот почему взаимно рекурсивные макросы не могут работать любым полезным способом.
Подумайте, что система, которая хочет оценить (или скомпилировать) код Lisp для немного более простого Lisp, чем CL (поэтому я избегаю некоторых тонкостей, которые происходят в CL), таких как определение функции, должна делать. У него очень мало вещей, которые он умеет делать:
- знает, как вызывать функции;
- знает, как оценивать несколько видов буквальных объектов;
- у него есть некоторые специальные правила для нескольких видов форм - то, что CL называет «специальными формами», которые (опять же на языке CL) - это формы, машина которых является специальным оператором;
- наконец, он знает, как посмотреть, соответствуют ли формы функциям, которые он может вызывать для преобразования кода, который он пытается оценить или скомпилировать - некоторые из этих функций предопределены, но могут быть определены дополнительные.
Таким образом, работа оценщика заключается в том, чтобы пройтись по тому, что ему нужно для оценки, ища эти вещи, преобразующие исходный код, aka макросов (последний случай), вызывая их функции и затем возвращаясь к результаты, пока он не заканчивается кодом, который не имеет ни одного. То, что осталось, должно состоять только из экземпляров первых трех случаев, с которыми оно затем знает, что делать.
Итак, теперь подумайте, что должен делать оценщик, если он оценивает определение функции, соответствующей макросу, называемому a
. В языке Cl он говорит, что оценивает или компилирует макрофункцию a
(которую вы можете получить через (macro-function 'a)
в CL). Предположим, что в некоторый момент в этом коде есть форма (b ...)
, и что, как известно, b
также соответствует макросу.
Итак, в какой-то момент дело доходит до (b ...)
, и он знает, что для этого ему нужно вызвать макро-функцию b
. Он связывает подходящие аргументы, и теперь ему нужно оценить определение тела этой функции ...
... и когда он делает это, он сталкивается с выражением типа (a ...)
. Что это должно сделать? Он должен вызывать макрофункцию a
, но не может, потому что он еще не знает, что это такое, потому что он находится в процессе разработки: он может начать пытаться снова ее обработать, но это просто цикл: он не попадет туда, где его еще не было.
Ну, есть ужасный трюк, который вы могли бы сделать, чтобы избежать этого. Вышеуказанный бесконечный регресс происходит из-за того, что оценщик пытается расширить все макросы заранее, и поэтому нет никаких оснований для рекурсии. Но давайте предположим, что определение макрофункции a
имеет код, который выглядит следующим образом:
(if <something>
(b ...)
<something not involving b>)
Вместо того, чтобы выполнять трюк «сначала все макросы», вы можете расширить только те макросы, которые вам нужны , непосредственно перед тем, как вам нужны их результаты. И если <something>
всегда оказывалось ложным, то вам никогда не нужно расширять (b ...)
, поэтому вы никогда не попадете в этот порочный круг: рекурсия заканчивается.
Но это означает, что вы всегда должны расширять макросы по требованию : вы никогда не сможете сделать это раньше времени, а поскольку макросы расширяются до исходного кода , вы никогда не сможете скомпилировать. Другими словами, подобная стратегия несовместима с компиляцией. Это также означает, что если <something>
когда-либо окажется правдой, то вы снова окажетесь в бесконечном регрессе.
Обратите внимание, что это полностью отличается от макросов, которые расширяют до кода, который включает тот же макрос, или другого макроса, который расширяется до кода, который его использует. Вот определение макроса с именем et
, который делает это (это, конечно, не нужно делать, это просто для того, чтобы это произошло):
(defmacro et (&rest forms)
(if (null forms)
't
`(et1 ,(first forms) ,(rest forms))))
(defmacro et1 (form more)
(let ((rn (make-symbol "R")))
`(let ((,rn ,form))
(if ,rn
,rn
(et ,@more)))))
Теперь (et a b c)
расширяется до (et1 a (b c))
, который расширяется до (let ((#:r a)) (if #:r #:r (et b c)))
(где все незатронутые вещи - одно и то же) и так далее, пока вы не получите
(let ((#:r a))
(if #:r
#:r
(let ((#:r b))
(if #:r
#:r
(let ((#:r c))
(if #:r
#:r
t))))))
Там, где теперь не все непрошенные символы одинаковы
И с правдоподобным макросом для let
(let
на самом деле является специальным оператором в CL), это может быть превращено еще дальше в
((lambda (#:r)
(if #:r
#:r
((lambda (#:r)
(if #:r
#:r
((lambda (#:r)
(if #:r
#:r
t))
c)))
b)))
a)
И это пример «вещей, с которыми система знает, как иметь дело»: все, что здесь осталось, это переменные, lambda
, примитивные условные и функциональные вызовы.
Одна из приятных сторон CL - это то, что, несмотря на то, что в ней много полезного сахара, вы все равно можете копаться в кишках вещей, если хотите. И, в частности, вы все еще видите, что макросы - это просто функции, которые преобразуют исходный код. Следующее делает именно то, что делают defmacro
версии (не совсем: defmacro
делает все необходимое, чтобы убедиться, что макросы доступны достаточно рано: мне нужно использовать eval-when
, чтобы сделать это с помощью ниже):
(setf (macro-function 'et)
(lambda (expression environment)
(declare (ignore environment))
(let ((forms (rest expression)))
(if (null forms)
't
`(et1 ,(first forms) ,(rest forms))))))
(setf (macro-function 'et1)
(lambda (expression environment)
(declare (ignore environment))
(destructuring-bind (_ form more) expression
(declare (ignore _))
(let ((rn (make-symbol "R")))
`(let ((,rn ,form))
(if ,rn
,rn
(et ,@more)))))))