非递归符号宏

650*_*502 6 lisp macros metaprogramming common-lisp

我想x将某个片段中的所有符号展开(value x).例如

(lambda ()
  (* x x))
Run Code Online (Sandbox Code Playgroud)

应该成为

(lambda ()
  (* (value x) (value x)))
Run Code Online (Sandbox Code Playgroud)

简单的使用是symbol-macrolet行不通的,因为

(symbol-macrolet ((x (value x)))
  (lambda ()
    (* x x)))
Run Code Online (Sandbox Code Playgroud)

在宏扩展期间爆炸成无限递归,因为扩展的结果symbol-macrolet再次被处理以用于宏扩展,包括相同的扩展symbol-macrolet.

甚至试图扩大x(value y),然后yx不工作.例如:

(symbol-macrolet ((y x))
  (symbol-macrolet ((x (value y)))
    (lambda () (* x x))))
Run Code Online (Sandbox Code Playgroud)

在SBCL的宏扩展时间仍然崩溃.

有没有办法只在没有完整代码行走的情况下扩展符号一次?

Jos*_*lor 8

这是在2013年comp.lang.lisp线程中讨论的.一位用户jathd指出:

您观察到的行为来自宏扩展的工作方式:处理表单以进行评估(或编译,或宏展开,或......)时,如果是宏调用,请将其替换为相应的扩展,并从中开始处理从新表格开始.

所以你必须积极地为符号宏做一些特殊的事情,而不是普通的宏,因为它们不是"递归的".

Pascal Constanza提供了以下建议:

一个好的解决方案是使符号宏扩展为常规宏.

(macrolet ((regular-macro (...) ...)) 
   (symbol-macrolet ((sym (regular-macro))) 
     ...))
Run Code Online (Sandbox Code Playgroud)

虽然informatimago指出这仍然表现出与原作相同的行为:

如果常规宏扩展包含在宏扩展位置,则该符号将命名为符号宏,这仍然会失败.

不幸的是,"有没有一种方法可以在没有完整的代码行走的情况下扩展符号一次?"似乎是"不".但是,要解决这个问题并不困难; 链接线程中的"解决方案"最终使用gensyms来避免问题.例如:

(let ((x 32))                            ; just to provide a value for x
  (let ((#1=#:genx x))                   ; new variable with x's value
    (symbol-macrolet ((x (values #1#)))  ; expansion contains the new variable
      (* x x))))                         ; === (* (values #1#) (values #1#))
;=> 1024
Run Code Online (Sandbox Code Playgroud)

#1#宏观扩展中的写作或类似事情并不好玩.如果您自动生成扩展,这并不算太糟糕,但如果您手动执行此操作,则利用let可能影子的事实可能会有用symbol-macrolet.这意味着您可以将扩展包装在let还原所需的绑定中:

(let ((x 32))
  (let ((#1=#:genx x))
    (symbol-macrolet ((x (let ((x #1#))    ; boilerplate
                           (values x))))   ; you get to refer to `x` here
      (* x x))))
;=> 1024
Run Code Online (Sandbox Code Playgroud)

如果你发现自己经常这么做,你可以将它包装在symbol-macrolet的"unhadowing"版本中:

(defmacro unshadowing-symbol-macrolet (((var expansion)) &body body)
  "This is like symbol-macrolet, except that var, which should have a binding
   in the enclosing environment, has that same binding within the expansion of
   the symbol macro.  This implementation only handles one var and expansion;
   extending to n-ary case is left as an exercise for the reader."
  (let ((hidden-var (gensym (symbol-name var))))
    `(let ((,hidden-var ,var))
       (symbol-macrolet ((,var (let ((,var ,hidden-var))
                                 ,expansion)))
         ,@body))))

(let ((x 32))
  (unshadowing-symbol-macrolet ((x (values x)))
    (* x x)))
;=> 1024
Run Code Online (Sandbox Code Playgroud)

当然,这只适用于已经具有词法绑定的变量.除了在宏扩展中传递它们之外,Common Lisp不提供访问环境对象的方式.如果您的实现提供了环境访问,则可以使用unshadowing-symbol-macrolet检查每个var是否在环境中绑定,如果是,则提供本地阴影,如果不是则不提供阴影.

笔记

有趣的是,看看这个主题的原作者Antsan不得不说出他们对宏扩展过程如何工作的期望:

我认为宏扩展通过在源上反复进行宏扩展直到达到修复点为止.这样,如果通过宏扩展删除了SYMBOL-MACROLET,它将自动是非递归的.

就像是:

(symbol-macrolet (a (foo a)) 
  a) 
macroexpand-1> (foo a) 
macroexpand-1> (foo a) ; fixpoint
Run Code Online (Sandbox Code Playgroud)

那里不需要特殊情况,虽然我猜这种宏扩展算法会慢一些.

这很有意思,因为这正是Common Lisp的编译器宏的工作方式.来自的文件define-compiler-macro说:

  • 与普通的宏不同,编译器宏只能通过返回与原始格式相同的格式(可以通过使用&whole获得)来拒绝提供扩展.

这在这里确实没有用,因为符号宏无法选择返回的内容; 也就是说,没有参数传递给符号宏,因此没有什么可以检查或用来影响宏展开的内容.返回相同形式的唯一方法就是这样(symbol-macrolet ((x x)) …),而不是失败的目的.