虽然上面的答案是在 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 与常规 Scheme 的 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% 解决方案。它在大多数情况下都可以工作,但在某些情况下会中断,特别是如果您将以这种方式编写的捕获宏合并到另一个宏中。如果expr 来自与true-expr 相同的范围,则上述宏将仅捕获it。如果它们来自不同的范围(这可以通过将用户的expr 包装在由您的宏生成的表单中来实现),那么true-expr 中的it 将不会被捕获,您会问自己“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 宏定义的,则上述将按预期工作。但是如果它是通过在true-expr 上使用datum->syntax 来定义的,那么将begin 添加到主体将导致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 无法捕获alias-it 的宏扩展中的it,因为直到aif 完成扩展后它才会出现在AST 上。
根本不可能修复这个问题,因为alias-it 的宏定义很可能在aif 的宏定义范围内是不可见的。因此,当您尝试在aif 中扩展它时,也许通过使用expand,alias-it 将被视为一个函数。测试表明,附加到alias-it 的词法信息不会使其被识别为超出alias-it 范围的宏定义的宏。
有些人会争辩说,这说明了为什么语法参数解决方案是更好的解决方案,但也许它真正说明的是为什么用 Common Lisp 编写代码是更好的解决方案。