尽管上面的答案是在 Racket 社区中实现 aif 的公认方式,但它存在严重缺陷。具体来说,您可以it
通过定义一个名为it
.
(let ((it 'gets-in-the-way))
(aif 'what-i-intended
(display it)))
上面将显示gets-in-the-way
而不是what-i-intended
,即使aif
定义了自己的变量名为it
. 外部let
形式使aif
的内部let
定义不可见。这就是 Scheme 社区想要发生的事情。实际上,他们希望您编写行为如此糟糕的代码,以至于当我不承认他们的方式更好时,他们投票决定删除我的原始答案。
在 Scheme 中编写捕获宏没有没有错误的方法。最接近的方法是沿着可能包含要捕获的变量的语法树向下走,并显式剥离它们包含的范围信息,用新的范围信息替换它,迫使它们引用这些变量的本地版本。我写了三个“for-syntax”函数和一个宏来帮助解决这个问题:
(begin-for-syntax
(define (contains? atom stx-list)
(syntax-case stx-list ()
(() #f)
((var . rest-vars)
(if (eq? (syntax->datum #'var)
(syntax->datum atom))
#t
(contains? atom #'rest-vars)))))
(define (strip stx vars hd)
(if (contains? hd vars)
(datum->syntax stx
(syntax->datum hd))
hd))
(define (capture stx vars body)
(syntax-case body ()
(() #'())
(((subform . tl) . rest)
#`(#,(capture stx vars #'(subform . tl)) . #,(capture stx vars #'rest)))
((hd . tl)
#`(#,(strip stx vars #'hd) . #,(capture stx vars #'tl)))
(tl (strip stx vars #'tl)))))
(define-syntax capture-vars
(λ (stx)
(syntax-case stx ()
((_ (vars ...) . body)
#`(begin . #,(capture #'(vars ...) #'(vars ...) #'body))))))
这为您提供了capture-vars
宏,它允许您从要捕获的主体中显式命名变量。aif
那么可以这样写:
(define-syntax aif
(syntax-rules ()
((_ something true false)
(capture-vars (it)
(let ((it something))
(if it true false))))
((_ something true)
(aif something true (void)))))
请注意,aif
我定义的工作类似于常规方案if
,因为 else 子句是可选的。
与上面的答案不同,it
是真正被捕获的。它不仅仅是一个全局变量:
(let ((it 'gets-in-the-way))
(aif 'what-i-intended
(display it)))
仅使用一次调用的不足之处datum->syntax
有些人认为创建捕获宏所需要做的就是datum->syntax
在传递给宏的顶级表单之一上使用,如下所示:
(define-syntax aif
(λ (stx)
(syntax-case stx ()
((_ expr true-expr false-expr)
(with-syntax
((it (datum->syntax #'expr 'it)))
#'(let ((it expr))
(if it true-expr false-expr))))
((_ expr true-expr)
#'(aif expr true-expr (void))))))
仅使用datum->syntax
只是编写捕获宏的 90% 解决方案。它在大多数情况下都可以工作,但在某些情况下会中断,特别是如果您将以这种方式编写的捕获宏合并到另一个宏中。仅it
当expr
来自与true-expr
. 如果它们来自不同的范围(这可以通过将用户包装在expr
由您的宏生成的表单中来实现),那么it
intrue-expr
将不会被捕获,您会问自己“WTF 不会捕获吗?”
您可能很想通过使用(datum->syntax #'true-expr 'it)
而不是(datum->syntax #'expr 'it)
. 实际上,这使问题变得更糟,因为现在您将无法使用aif
以下定义acond
:
(define-syntax acond
(syntax-rules (else)
((_) (void))
((_ (condition . body) (else . else-body))
(aif condition (begin . body) (begin . else-body)))
((_ (condition . body) . rest)
(aif condition (begin . body) (acond . rest)))))
如果aif
使用capture-vars
宏定义,上述将按预期工作。但是如果它是通过使用datum->syntax
on 定义的true-expr
,添加begin
到 body 将导致在的宏定义it
范围内可见,acond
而不是在调用的代码中可见acond
。
在 Racket 中真正编写捕获宏是不可能的
这个例子引起了我的注意,并说明了为什么你不能在 Scheme 中编写一个真正的捕获宏:
(define-syntax alias-it
(syntax-rules ()
((_ new-it . body)
(let ((it new-it)) . body))))
(aif (+ 1 2) (alias-it foo ...))
capture-vars
无法捕获it
in的宏扩展,因为在完成扩展alias-it
之前它不会在 AST 上。aif
根本不可能解决这个问题,因为从's 宏定义alias-it
的范围内很可能看不到的宏定义。aif
因此,当您尝试在 内扩展它时aif
,也许通过使用expand
,alias-it
将被视为一个函数。测试表明,附加到 的词法信息alias-it
不会导致它被识别为宏定义的宏,该宏定义从alias-it
.
有人会争辩说,这说明了为什么语法参数解决方案是更好的解决方案,但也许它真正说明的是为什么用 Common Lisp 编写代码是更好的解决方案。