Разве любой Лисп разрешает взаимно рекурсивные макросы? - PullRequest
1 голос
/ 20 мая 2019

В Common Lisp определение макроса должно быть видно перед первым использованием.Это позволяет макросу ссылаться на себя, но не позволяет двум макросам ссылаться друг на друга.Ограничение немного неловко, но понятно;это немного упрощает реализацию системы макросов и позволяет понять, как работает реализация.

Существует ли какой-либо язык семейства Lisp, в котором два макроса могут ссылаться друг на друга?

Ответы [ 3 ]

4 голосов
/ 21 мая 2019

Что такое макрос?

Макрос - это просто функция, которая вызывается для кода , а не данных .

Например, когдавы пишете

(defmacro report (x)
  (let ((var (gensym "REPORT-")))
    `(let ((,var ,x))
       (format t "~&~S=<~S>~%" ',x ,var)
       ,var)))

вы на самом деле определяете функцию , которая выглядит примерно так:

(defun macro-report (system::<macro-form> system::<env-arg>)
  (declare (cons system::<macro-form>))
  (declare (ignore system::<env-arg>))
  (if (not (system::list-length-in-bounds-p system::<macro-form> 2 2 nil))
      (system::macro-call-error system::<macro-form>)
      (let* ((x (cadr system::<macro-form>)))
        (block report
          (let ((var (gensym "REPORT-")))
            `(let ((,var ,x)) (format t "~&~s=<~s>~%" ',x ,var) ,var))))))

Т.е. когда вы пишете, скажем,

(report (! 12))

lisp фактически передает форму (! 12) в качестве 1-го аргумента macro-report, который преобразует ее в :

(LET ((#:REPORT-2836 (! 12)))
  (FORMAT T "~&~S=<~S>~%" '(! 12) #:REPORT-2836)
  #:REPORT-2836)

и только затем вычисляет ее для вывода (! 12)=<479001600> иreturn 479001600.

Рекурсия в макросах

Существует разница, вызывает ли себя сам макрос в реализации или в расширении .

Например, возможная реализация макроса and:

(defmacro my-and (&rest args)
  (cond ((null args) T)
        ((null (cdr args)) (car args))
        (t
         `(if ,(car args)
              (my-and ,@(cdr args))
              nil))))

Обратите внимание, что может расширяться в себя:

(macroexpand '(my-and x y z))
==> (IF X (MY-AND Y Z) NIL) ; T

Как видите, расширение макроса содержит определяемый макрос.Это не проблема, например, (my-and 1 2 3) правильно оценивается как 3.

Однако, если мы попытаемся реализовать макрос, использующий сам себя, например,

(defmacro bad-macro (code)
  (1+ (bad-macro code)))

вы получите сообщение об ошибке (переполнение стека или неопределенная функция или ...) при попытке его использования, в зависимости от реализации.

2 голосов
/ 23 мая 2019

Существовали исторические системы Lisp, которые допускают это, по крайней мере, в интерпретируемом коде.

Мы можем позволить макросу использовать себя для своего собственного определения или два или более макросов для взаимного использования друг друга, еслимы придерживаемся крайне поздней стратегии расширения.

То есть наша система макросов расширяет вызов макроса непосредственно перед его оценкой (и делает это каждый раз, когда вычисляется одно и то же выражение).

(Такая стратегия расширения макроса хороша для интерактивной разработки с макросами. Если вы исправите ошибочный макрос, то весь код, зависящий от него, автоматически получит выгоду от изменения, без необходимости повторной обработки каким-либо образом.)

В такой системе макросов предположим, что у нас есть такое условие:

(if (condition)
  (macro1 ...)
  (macro2 ...))

Когда (condition) вычисляется, то, если оно возвращает true, (macro1 ...) оценивается, в противном случае (macro2 ...).Но оценка также означает расширение.Таким образом, раскрыт только один из этих двух макросов.

Это ключ к тому, почему взаимные ссылки между макросами могут работать: мы можем полагаться на условную логику, чтобы дать нам не только условную оценку, но и условное расширение,что затем позволяет рекурсии иметь способы завершения.

Например, предположим, что тело кода макроса A определяется с помощью макроса B, а наоборот ,И когда выполняется конкретный вызов A, он попадает в тот конкретный случай, который требует B, и поэтому вызов B расширяется путем вызова макроса B.B также попадает в регистр кода, который зависит от A, и поэтому возвращается в A для получения необходимого расширения.Но на этот раз A вызывается таким образом, чтобы избежать необходимости, опять же, расширения B;он избегает вычисления любого под-выражения, содержащего макрос B.Таким образом, он вычисляет расширение и возвращает его в B, который затем вычисляет , его расширение возвращает к внешнему A.A наконец расширяется и рекурсия заканчивается;все хорошо.

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


Обратите внимание, кстати, что система макросов, которая расширяется поздно, не рекурсивно расширяет макросы в макросерасширение.Предположим, что (mac1 x y) расширяется до (if x (mac2 y) (mac3 y)).Ну, вот и все расширение, которое сделано на данный момент: всплывающее if не является макросом, поэтому расширение останавливается, и оценка продолжается.Если x возвращает true, тогда mac2 расширяется, а mac3 - нет.

2 голосов
/ 21 мая 2019

Вот почему взаимно рекурсивные макросы не могут работать любым полезным способом.

Подумайте, что система, которая хочет оценить (или скомпилировать) код 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)))))))
...