【问题标题】:Capturing Macros in Scheme在 Scheme 中捕获宏
【发布时间】:2013-12-01 23:39:09
【问题描述】:

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

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

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

想法是it 将绑定到if-trueif-false 子句中test 的结果。天真的音译(减去可选的替代)是

(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 eggaif 实现为

(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)))))))))

但 Racket 似乎没有定义或记录 ir-macro-transformer

【问题讨论】:

    标签: macros scheme racket define-syntax


    【解决方案1】:

    请参阅 Greg Hendershott 的宏教程。本节以照应 if 为例:

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

    【讨论】:

    • 或者更好,请参阅Greg Hendershott's answer。 :)
    • Greg Hendershott 在其中最终提到了 Eli Barzilay 的博客文章和论文......谢天谢地,我们已经消除了尾巴...... :)
    【解决方案2】:

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

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

    但在 Racket 中,执行“照应”宏最简单、最安全的方法是使用 syntax parameter 和简单的 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)))
    

    我写了关于语法参数 here 的文章,你也应该阅读 Eli Barzilay 的 Dirty Looking Hygiene blog postKeeping it Clean with Syntax Parameters paper (PDF)

    【讨论】:

    • TBH,这篇博文并没有真正的相关性,因为这篇文章对它的介绍更好,并且有更多的例子......
    • @EliBarzilay 虽然我想为任何可能是“OMG PDF!”的人添加博客文章。或“OMG 研究论文!”,我同意这篇论文是最好的。
    • 是的,我知道有些人会有这种反应——只是那篇论文的开头部分很容易理解......但是,是的,只有一篇博文确实有点价值...
    • @ThrowawayAccount3Million 正如最后一段here 解释说:“生成的宏不会破坏卫生。例如,(let ([it 3]) (if #t it)) 的计算结果为3,因为它会影响@ 的全局it 987654334@ 更改。这是对真正不卫生的宏的更改——但这就是重点:我们(宏作者)不会干扰用户代码中的范围。”
    • 当您真正需要捕获,或者想要捕获以捕获,您可以使用datum->syntax,就像我提到的那样。有了它,一切皆有可能,无论好坏。但要备份:我不会将aif 用于真实代码。我认为一个更好的主意是像if-let,你提供id。通常,当宏的用户提供标识符时,它会更清晰、更可靠。 (显然有像struct 这样的例外,但至少引入了以用户提供的 id 为前缀的名称。)
    【解决方案3】:

    虽然上面的答案是在 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 添加到主体将导致itacond 的宏定义范围内可见,而不是调用的代码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 中扩展它时,也许通过使用expandalias-it 将被视为一个函数。测试表明,附加到alias-it 的词法信息不会使其被识别为超出alias-it 范围的宏定义的宏。

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

    【讨论】:

    • 投反对票不是因为你试图进入愚蠢的火焰战争,而是因为它不是原始问题的答案。您的答案(也许在您的理解中)存在三个基本问题。这会很长,所以我将它分成几个 cmets。
    • (A) 是的,使用语法参数意味着绑定可以被隐藏——这个可取的原因是it 语法参数不被视为作为一个全局变量,但作为 aif 宏的一部分 -- 所以遮蔽 it 等同于遮蔽 aif,并且(显然)会破坏它。 (注意,it 应该在定义 aif 的任何地方定义 - 这意味着它与 aif 一样具有全局性,在球拍中,这通常意味着它是在某个模块中定义的(即,它仍然不是“全局的” ).) 这意味着您对语法参数的批评是错误的。
    • (B) 你的“写一个真正的捕获宏是不可能的”结论是错误的。您可以通过首先扩展输入表单来修复它(例如,使用local-expand)。不过,这样做并不是什么新鲜事:请参阅语法参数的文本,其中提到了剥离所有词汇信息(并且 CL 并没有将其保留在首位)。另请参阅 Racket 的 define-macro,它使简单的类似 CL 的 aif 定义工作正常,并且在 acond 甚至与 alias-it 一起使用时也应该工作。 (define-macro 正在做你真正想做的事情,只是以更有条理和有条理的方式。
    • (C) 最后,主要问题是,它真的没有回答问题。如果您真的坚持这个方向,请将人们指向define-macro 并完成它。至于你的长篇文章,你应该更多地研究这个,更好地表达它,并将它发布在适合此类讨论的地方。 Racket 邮件列表就是这样一个地方——如果你坚持细节而不是被冒犯,结果肯定会更好。从设计上讲,SO 并不是进行此类讨论的好地方。这也是为什么这些“评论-讨论”很难做到的原因,所以我就到此为止。
    • 我会进一步研究,但我在过去让define-macro 做这种事情时遇到了问题,这就是我没有提及它的原因。我在答案中提到的同样类型的问题,只要宏不属于另一个宏的一部分,它就可以工作。当列表仍然可变时,我放弃了它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-24
    • 2010-12-21
    • 1970-01-01
    • 1970-01-01
    • 2014-03-23
    • 1970-01-01
    相关资源
    最近更新 更多