7

define-syntax使用或define-syntax-rule在 Racket中定义捕获宏的最简单方法是什么?

作为一个具体的例子,这里是aif一个 CL 风格的宏系统中的琐碎。

(defmacro aif (test if-true &optional if-false)
    `(let ((it ,test))
        (if it ,if-true ,if-false)))

这个想法是it绑定到testandif-true子句if-false的结果。天真的音译(减去可选替代项)是

(define-syntax-rule (aif test if-true if-false)
    (let ((it test))
       (if it if-true if-false)))

it如果您尝试在子句中使用,它会毫无怨言地进行评估,但会出错:

> (aif "Something" (displayln it) (displayln "Nope")))
reference to undefined identifier: it

anaphora鸡蛋实现aif为_

(define-syntax aif
  (ir-macro-transformer
   (lambda (form inject compare?)
     (let ((it (inject 'it)))
       (let ((test (cadr form))
         (consequent (caddr form))
         (alternative (cdddr form)))
     (if (null? alternative)
         `(let ((,it ,test))
        (if ,it ,consequent))
         `(let ((,it ,test))
        (if ,it ,consequent ,(car alternative)))))))))

但球拍似乎没有ir-macro-transformer定义或记录。

4

3 回答 3

11

Racket 宏旨在避免默认捕获。当您使用define-syntax-rule它时,它将尊重词法范围。

当您想故意“破坏卫生”时,传统上在 Scheme 中您必须使用syntax-case和(小心)使用datum->syntax.

但在 Racket 中,执行“照应”宏最简单、最安全的方法是使用语法参数和简单的define-syntax-rule.

例如:

(require racket/stxparam)

(define-syntax-parameter it
  (lambda (stx)
    (raise-syntax-error (syntax-e stx) "can only be used inside aif")))

(define-syntax-rule (aif condition true-expr false-expr)
  (let ([tmp condition])
    (if tmp
        (syntax-parameterize ([it (make-rename-transformer #'tmp)])
          true-expr)
        false-expr)))

在这里写了关于语法参数的文章,您还应该阅读 Eli Barzilay 的Dirtylooking Hygiene博客文章使用语法参数保持清洁(PDF)

于 2013-11-18T18:27:37.010 回答
4

请参阅 Greg Hendershott 的宏教程。本节使用照应 if 作为示例:

http://www.greghendershott.com/fear-of-macros/Syntax_parameters.html

于 2013-11-18T18:12:04.320 回答
2

尽管上面的答案是在 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% 解决方案。它在大多数情况下都可以工作,但在某些情况下会中断,特别是如果您将以这种方式编写的捕获宏合并到另一个宏中。仅itexpr来自与true-expr. 如果它们来自不同的范围(这可以通过将用户包装在expr由您的宏生成的表单中来实现),那么itintrue-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->syntaxon 定义的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无法捕获itin的宏扩展,因为在完成扩展alias-it之前它不会在 AST 上。aif

根本不可能解决这个问题,因为从's 宏定义alias-it的范围内很可能看不到的宏定义。aif因此,当您尝试在 内扩展它时aif,也许通过使用expand,alias-it将被视为一个函数。测试表明,附加到 的词法信息alias-it不会导致它被识别为宏定义的宏,该宏定义从alias-it.

有人会争辩说,这说明了为什么语法参数解决方案是更好的解决方案,但也许它真正说明的是为什么用 Common Lisp 编写代码是更好的解决方案。

于 2015-05-02T22:52:11.457 回答