【问题标题】:Does any Lisp allow mutually recursive macros?是否有任何 Lisp 允许相互递归的宏?
【发布时间】:2026-01-27 23:50:01
【问题描述】:

在 Common Lisp 中,必须在第一次使用之前看到宏定义。这允许宏引用自身,但不允许两个宏相互引用。限制有点尴尬,但可以理解;它使宏系统更容易实现,并且更容易理解实现的工作原理。

是否有任何 Lisp 家族语言可以让两个宏相互引用?

【问题讨论】:

  • 展示您尝试编写的递归宏的示例。
  • 很难想象有任何实现可以做到这一点。宏在编译期间被扩展,因此编译第一个宏需要扩展第二个宏,但它尚未定义。只有解释器实现才能避免这个问题,因为它不编译宏。
  • 这就是为什么 Lisp 编译器是用 Lisp 编写的。他们可以编译宏,然后当他们遇到宏的用户时,他们可以调用编译后的函数进行扩展。
  • 如果宏调用自己就不行。如果宏扩展为自身的使用,它就可以工作,就像相互递归的宏一样。
  • 我不确定您是否理解在实现中使用和在扩展中使用之间的区别。同样,如果您展示您正在考虑的相关宏的实际示例,我可以说明它是否可能。

标签: macros lisp common-lisp


【解决方案1】:

什么是宏?

宏只是一个在代码而不是数据上调用的函数。

例如,当你写作时

(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) 形式作为第一个参数传递给macro-report,后者将其转换为into

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

然后才评估它以打印 (! 12)=&lt;479001600&gt; 并返回 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】:

    这就是为什么相互递归的宏不能以任何有用的方式工作的原因。

    考虑一个系统想要评估(或编译)Lisp 代码以获得比 CL 稍微简单的 Lisp(所以我要避免 CL 中发生的一些微妙之处),例如函数的定义,需要做。它知道如何做的事情很少:

    • 它知道如何调用函数;
    • 它知道如何评估几种文字对象;
    • 它对几种表格有一些特殊的规则——CL 称之为“特殊表格”,它(同样用 CL 来说)是汽车是特殊运算符的表格;
    • 最后它知道如何查看表单是否对应于它可以调用的函数来转换它试图评估或编译的代码 - 其中一些函数是预定义的,但可以定义其他函数。

    所以评估器的工作方式是遍历它需要评估的东西,寻找这些源代码转换的东西,aka 宏(最后一种情况),调用它们的函数,然后递归在结果上,直到它以没有剩余的代码结束。剩下的应该只包含前三种情况的实例,然后它知道如何处理。

    所以现在考虑一下,如果评估器正在评估与名为a 的宏对应的函数的定义,它必须做什么。在 Cl-speak 中,它正在评估或编译 a 的宏函数(您可以通过 CL 中的 (macro-function 'a) 获得)。让我们假设这段代码中有一个 (b ...) 的形式,并且已知 b 也对应于一个宏。

    所以在某个时候它涉及到(b ...),它知道为了做到这一点,它需要调用b 的宏函数。它绑定了合适的参数,现在它需要评估该函数主体的定义......

    ... 当它执行此操作时会遇到类似(a ...) 的表达式。它应该怎么做?它需要调用a 的宏函数,但它不能,因为它还不知道它是什么,因为它正在解决这个问题:它可以开始尝试再次解决它,但是这只是一个循环:它不会到达它尚未到达的任何地方。

    嗯,你可以做一个可怕的把戏来避免这种情况。上面的无限回归发生是因为评估器试图提前扩展所有宏,因此递归没有基础。但是我们假设a的宏函数的定义有如下代码:

    (if <something>
        (b ...)
        <something not involving b>)
    

    您可以做的不是先扩展所有宏,而是在需要结果之前仅扩展您需要的宏。如果&lt;something&gt; 总是为假,那么你永远不需要扩展(b ...),所以你永远不会陷入这个恶性循环:递归触底。

    但这意味着您必须始终按需扩展宏:您永远无法提前完成,而且由于宏扩展为源代码,您永远无法编译。换句话说,这样的策略与编译不兼容。这也意味着如果&lt;something&gt; 被证明是真的,那么你将再次陷入无限倒退。


    请注意,这与将宏扩展为涉及相同宏的代码或扩展为使用它的代码的另一个宏完全不同。这是一个名为 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)))))))
    

    【讨论】:

      【解决方案3】:

      历史悠久的 Lisp 系统允许这样做,至少在解释代码中。

      如果我们遵循极晚的扩展策略,我们可以允许一个宏将自己用于自己的定义,或者两个或多个宏相互使用。

      也就是说,我们的宏系统在对宏调用进行评估之前对其进行扩展(并且每次对相同的表达式进行评估时都会这样做)。

      (这样的宏扩展策略有利于使用宏进行交互式开发。如果您修复了有问题的宏,那么依赖于它的所有代码都会自动受益于更改,而无需以任何方式重新处理。)

      在这样一个宏系统下,假设我们有这样一个条件:

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

      (condition) 被评估时,如果结果为真,(macro1 ...) 被评估,否则(macro2 ...)。但评估也意味着扩展。因此只有这两个宏中的一个被展开。

      这就是为什么宏之间的相互引用可以起作用的关键:我们能够依靠条件逻辑不仅给我们条件评估,还给我们条件扩展,然后允许递归有终止的方式。

      例如,假设宏A 的代码体是在宏B 的帮助下定义的,反之亦然。并且当执行A 的特定调用时,它恰好遇到需要B 的特定情况,因此B 调用通过调用宏B 来扩展。 B 也命中依赖于A 的代码案例,因此它递归到A 以获得所需的扩展。但是,这一次,A 的调用方式避免了再次需要 B 的扩展;它避免评估任何包含B 宏的子表达式。因此,它计算展开,并将其返回到B,然后计算展开返回到最外层的AA终于展开,递归终止;一切都很好。

      阻止宏相互使用的是无条件扩展策略:整个顶层表单被读取后完全扩展的策略,使得函数和宏的定义只包含扩展代码。在这种情况下,不可能进行允许递归终止的条件扩展。


      顺便提一下,后期扩展的宏系统不会在宏扩展中递归地扩展宏。假设(mac1 x y) 扩展为(if x (mac2 y) (mac3 y))。好吧,这就是现在完成的所有扩展:弹出的if 不是宏,因此扩展停止,并且评估继续进行。如果x 的结果为真,则mac2 被扩展,而mac3 不是。

      【讨论】: