这个问题在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))
(let ((#1=#:genx x))
(symbol-macrolet ((x (values #1#)))
(* x x))))
在宏展开中写入#1#
或类似的内容并不好玩。如果你是自动生成扩展,那么情况还不错,但如果你是手动完成这个过程,利用let
可以屏蔽symbol-macrolet
的事实可能会很有用。这意味着你可以用一个let
来包装你的扩展,然后恢复你想要的绑定:
(let ((x 32))
(let ((#1=#:genx x))
(symbol-macrolet ((x (let ((x #1#))
(values x))))
(* x x))))
如果你经常这样做,你可以将它包装在一个“取消阴影”版本的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)))
当然,这仅适用于已经具有词法绑定的变量。除了在宏扩展中传递它们之外,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)
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)) …)
,这样做有点违背了初衷。