Macros 任何Lisp都允许相互递归宏吗?

Macros 任何Lisp都允许相互递归宏吗?,macros,lisp,common-lisp,Macros,Lisp,Common Lisp,在Common Lisp中,宏定义必须在第一次使用之前已被看到。这允许宏引用自身,但不允许两个宏相互引用。限制有点尴尬,但可以理解;它使宏系统更容易实现,也更容易理解实现是如何工作的 是否有两个宏可以相互引用的Lisp族语言?这就是为什么相互递归的宏不能以任何有用的方式工作 考虑一下,一个系统想要为比CL稍微简单的Lisp计算或编译Lisp代码,所以我要避免CL中发生的一些微妙之处,比如函数的定义,需要做什么。它知道如何做的事情很少: 它知道如何调用函数; 它知道如何评估几种文字对象; 它对几种

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


是否有两个宏可以相互引用的Lisp族语言?

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

考虑一下,一个系统想要为比CL稍微简单的Lisp计算或编译Lisp代码,所以我要避免CL中发生的一些微妙之处,比如函数的定义,需要做什么。它知道如何做的事情很少:

它知道如何调用函数; 它知道如何评估几种文字对象; 它对几种形式都有一些特殊的规定——CL称之为“特殊形式”,也就是说,在CL中,汽车是特殊操作员的形式; 最后,它知道如何查看表单是否对应于它可以调用的函数,以转换它试图计算或编译的代码——其中一些函数是预定义的,但可以定义其他函数。 因此,计算器的工作方式是遍历它需要评估的东西,寻找这些转换东西的源代码,最后一种情况是宏,调用它们的函数,然后对结果进行递归,直到最后得到一个没有剩下的代码。剩下的应该只包括前三个案例,然后它知道如何处理

现在想想,如果计算器正在计算与宏(称为a)对应的函数的定义,那么它必须做什么。在Cl语言中,它是计算或编译a的宏函数,你可以通过Cl中的宏函数“a”得到它。让我们假设在某一点上有一个形式b。。。在这段代码中,已知b也对应于宏

所以在某一点上,它涉及到b…,并且它知道为了做到这一点,它需要调用b的宏函数。它绑定了合适的参数,现在需要评估函数体的定义

。。。当它这样做的时候,它会遇到一个像。。。。它应该做什么?它需要调用一个宏函数,但它不能,因为它还不知道它是什么,因为它正处于工作的中间:它可以开始尝试重新计算它,但是这只是一个循环:它不会到达它还没有的任何地方。 好吧,你可以用一个可怕的技巧来避免这种情况。发生上述无限回归是因为计算器试图提前展开所有宏,因此递归没有基础。但我们假设a的宏函数的定义有如下代码:

(if <something>
    (b ...)
    <something not involving b>)
(if (condition)
  (macro1 ...)
  (macro2 ...))
现在etabc展开为et1abc,它展开为:raif:r:retbc,所有不相关的东西都是相同的东西,依此类推,直到你得到

(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来完成以下操作:

(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)))))))

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

考虑一下,一个系统想要为比CL稍微简单的Lisp计算或编译Lisp代码,所以我要避免CL中发生的一些微妙之处,比如函数的定义,需要做什么。它知道如何做的事情很少:

它知道如何调用函数; 它知道如何评估几种文字对象; 它对几种形式都有一些特殊的规定——CL称之为“特殊形式”,也就是说,在CL中,汽车是特殊操作员的形式; 最后,它知道如何查看表单是否对应于它可以调用的函数,以转换它试图计算或编译的代码——其中一些函数是预定义的,但可以定义其他函数。 因此,计算器的工作方式是遍历它需要评估的东西,寻找这些转换东西的源代码,最后一种情况是宏,调用它们的函数,然后对结果进行递归,直到最后得到一个没有剩下的代码。W 帽子的左边应该只包含前三个案例的实例,然后它知道如何处理

现在想想,如果计算器正在计算与宏(称为a)对应的函数的定义,那么它必须做什么。在Cl语言中,它是计算或编译a的宏函数,你可以通过Cl中的宏函数“a”得到它。让我们假设在某一点上有一个形式b。。。在这段代码中,已知b也对应于宏

所以在某一点上,它涉及到b…,并且它知道为了做到这一点,它需要调用b的宏函数。它绑定了合适的参数,现在需要评估函数体的定义

。。。当它这样做的时候,它会遇到一个像。。。。它应该做什么?它需要调用一个宏函数,但它不能,因为它还不知道它是什么,因为它正处于工作的中间:它可以开始尝试重新计算它,但是这只是一个循环:它不会到达它还没有的任何地方。 好吧,你可以用一个可怕的技巧来避免这种情况。发生上述无限回归是因为计算器试图提前展开所有宏,因此递归没有基础。但我们假设a的宏函数的定义有如下代码:

(if <something>
    (b ...)
    <something not involving b>)
(if (condition)
  (macro1 ...)
  (macro2 ...))
现在etabc展开为et1abc,它展开为:raif:r:retbc,所有不相关的东西都是相同的东西,依此类推,直到你得到

(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来完成以下操作:

(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)))))))
什么是宏? 宏只是对代码而不是数据调用的函数

例如,当你写作时

(defmacro report (x)
  (let ((var (gensym "REPORT-")))
    `(let ((,var ,x))
       (format t "~&~S=<~S>~%" ',x ,var)
       ,var)))
lisp实际上传递了表单!12作为转换宏报告的第一个参数:

请注意,它可能会扩展到自身:

(macroexpand '(my-and x y z))
==> (IF X (MY-AND Y Z) NIL) ; T
如您所见,宏扩展包含正在定义的宏。 这不是问题,例如,my 1和2 3正确计算为3

但是,如果我们尝试使用宏本身实现宏,例如

(defmacro bad-macro (code)
  (1+ (bad-macro code)))
您将在堆栈溢出或未定义函数或。。。当您尝试使用它时,具体取决于实现。

什么是宏? 宏只是对代码而不是数据调用的函数

例如,当你写作时

(defmacro report (x)
  (let ((var (gensym "REPORT-")))
    `(let ((,var ,x))
       (format t "~&~S=<~S>~%" ',x ,var)
       ,var)))
lisp实际上传递了表单!12作为转换宏报告的第一个参数:

请注意,它可能会扩展到自身:

(macroexpand '(my-and x y z))
==> (IF X (MY-AND Y Z) NIL) ; T
如您所见,宏扩展包含正在定义的宏。 这不是问题,例如,my 1和2 3正确计算为3

但是,如果我们尝试使用宏本身实现宏,例如

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

您将在堆栈溢出或未定义函数或。。。当您尝试使用它时,这取决于实现。

历史上有一些Lisp系统允许这样做,至少在解释代码中是这样

我们可以允许一个宏在自己的定义中使用它自己,或者允许两个或多个宏相互使用,如果我们遵循非常晚的扩展策略的话

也就是说,我们的宏系统在对宏调用求值之前展开宏调用,并且在每次对同一表达式求值时展开宏调用

这种宏扩展策略有利于与宏的交互开发。如果您修复了一个有缺陷的宏,那么所有依赖它的代码都会自动从更改中受益,而无需以任何方式重新处理

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

(if <something>
    (b ...)
    <something not involving b>)
(if (condition)
  (macro1 ...)
  (macro2 ...))
当条件被评估时,如果它产生真值,宏1。。。进行计算,否则宏2。。。。但评估也意味着扩张。因此,这两个宏中只有一个被展开

这就是为什么宏之间的相互引用可以工作的关键:我们可以依赖条件逻辑,不仅给我们提供条件求值,还提供条件扩展,从而允许递归有终止的方式

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

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


顺便说一下,一个后期扩展的宏系统不会在宏扩展中递归地扩展宏。假设mac1 x y展开为if x mac2 y mac3 y。好的,这就是现在所做的所有扩展:如果弹出的不是宏,那么扩展停止,评估继续。如果x的结果为真,那么mac2将被扩展,而mac3则不是。

历史上的Lisp系统至少在解释代码中允许这样做

我们可以允许一个宏在自己的定义中使用它自己,或者允许两个或多个宏相互使用,如果我们遵循非常晚的扩展策略的话

也就是说,我们的宏系统在对宏调用求值之前展开宏调用,并且在每次对同一表达式求值时展开宏调用

这种宏扩展策略有利于与宏的交互开发。如果您修复了一个有缺陷的宏,那么所有依赖它的代码都会自动从更改中受益,而无需以任何方式重新处理

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

(if <something>
    (b ...)
    <something not involving b>)
(if (condition)
  (macro1 ...)
  (macro2 ...))
当条件被评估时,如果它产生真值,宏1。。。进行计算,否则宏2。。。。但评估也意味着扩张。因此,这两个宏中只有一个被展开

这就是为什么宏之间的相互引用可以工作的关键:我们可以依赖条件逻辑,不仅给我们提供条件求值,还提供条件扩展,从而允许递归有终止的方式

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

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


顺便说一下,一个后期扩展的宏系统不会在宏扩展中递归地扩展宏。假设mac1 x y展开为if x mac2 y mac3 y。好的,这就是现在所做的所有扩展:如果弹出的不是宏,那么扩展停止,评估继续。如果x的结果为真,那么mac2将被扩展,而mac3则不是。

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