非递归符号宏定义

6

我想将某个片段中所有的符号 x 扩展为 (value x)。例如:

(lambda ()
  (* x x))

应变为

(lambda ()
  (* (value x) (value x)))

一个简单的 symbol-macrolet 的使用是不起作用的,因为:
(symbol-macrolet ((x (value x)))
  (lambda ()
    (* x x)))

在宏扩展期间,由于符号宏扩展的结果被再次处理用于宏扩展(包括同一符号宏扩展),导致无限递归,这是一个常见问题。
即使尝试将 x 扩展为 (value y),然后将 y 扩展为 x 也不起作用。例如:
(symbol-macrolet ((y x))
  (symbol-macrolet ((x (value y)))
    (lambda () (* x x))))

在SBCL上,它仍会在宏展开时崩溃。

有没有一种方法可以仅展开符号而不进行完整的代码遍历?


在扩展期间重命名x怎么样:(symbol-macrolet ((x (value y))) ...)? - cybevnm
@cybevnm:好主意,但显然不起作用。请看编辑。 - 6502
我之前在 Reddit 上也问过这个问题,得到了一些很好的讨论:http://www.reddit.com/r/lisp/comments/13nqrw/question_on_nonrecursive_macrolet/ - andrew
1个回答

8
这个问题在2013年的comp.lang.lisp thread中讨论过。用户jathd指出:
你观察到的行为来自宏展开的一般性质:在处理表达式进行求值(或编译、宏展开等)时,如果它是一个宏调用,则将其替换为相应的展开并从新形式开始处理。
所以,对于符号宏而言,你需要积极地做一些特殊的事情,以使它们不会“递归”,与常规宏不同。
Pascal Constanza提供了以下建议:

A good solution is to make the symbol macro expand into a regular macro.

(macrolet ((regular-macro (...) ...)) 
   (symbol-macrolet ((sym (regular-macro))) 
     ...))
尽管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

在宏展开中写入#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

如果你经常这样做,你可以将它包装在一个“取消阴影”版本的symbol-macrolet中:

(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

当然,这仅适用于已经具有词法绑定的变量。除了在宏扩展中传递它们之外,Common Lisp没有提供太多访问环境对象的方法。如果您的实现提供环境访问,则可以使unshadowing-symbol-macrolet检查每个 var 是否在环境中绑定,并在其绑定时提供本地遮盖,如果未绑定则不进行遮盖。
注释
有趣的是看到那个线程中原作者Antsan对他们对宏扩展过程的期望所说的话。

I thought macro expansion worked by repeatedly doing macro expansion on the source until a fixpoint is reached. This way SYMBOL-MACROLET would be automatically non-recursive if it was removed by macro expansion.

Something like:

(symbol-macrolet (a (foo a)) 
  a) 
macroexpand-1> (foo a) 
macroexpand-1> (foo a) ; fixpoint

No special case would be needed there, although I guess that this algorithm for macro expansion would be slower.

有趣的是,这正是Common Lisp的编译器宏的工作方式。define-compiler-macro的文档中写道:
与普通宏不同,编译器宏可以通过返回一个与原始表单相同的表单来拒绝提供扩展(可以使用&whole获得)。
这在这里并没有什么帮助,因为符号宏没有选择返回什么的权利;也就是说,符号宏没有传递参数,因此没有任何可以检查或用于影响宏展开的东西。唯一返回相同形式的方法是类似于(symbol-macrolet ((x x)) …),这样做有点违背了初衷。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接