【问题标题】:Racket: Macros generated by macros defined as macroname.0Racket:由定义为 macroname.0 的宏生成的宏
【发布时间】:2018-05-28 05:24:55
【问题描述】:

我正在尝试了解一下 Racket 及其宏系统。我的介绍是围绕IEX stock API 编写一个薄包装。我的准备读物包括 Greg Hendershott 的 Fear of MacrosRacket Guide(特别是关于宏生成宏和相关文档的部分)以及其他在线资源。

我希望能够做这样的事情:

(iex-op* chart dividends quote)

生成函数iex-chartiex-dividends等。语法转换器的使用对我来说似乎很自然,因为我认为我不能轻松地编写一个具有干净语法和多个参数的函数生成函数。

所以:

(define-syntax-rule (iex-op* op0 ...)
  (begin
    (iex-op op0) ...))

假设iex-op 语法转换器工作正常:

(define iex-base-url (string->url "https://api.iextrading.com"))
(define iex-ver "1.0")

(define-syntax (iex-op stx)
  (syntax-case stx (quote) ; quote is one of the operations
    [(_ oper)
     (with-syntax ([op-name (format-id stx "iex-~a" #'oper)])
         #'(define (op-name ticker . args)
             (let ([op-url
                    (combine-url/relative
                        iex-base-url
                        (string-join
                          `(,iex-ver
                            "stock"
                            ,(symbol->string ticker)
                            ,(symbol->string (syntax-e #'oper))
                            ,(string-join args "/")) "/"))])
              (string->jsexpr (http-response-body (get http-requester op-url))))))]))

我的问题不在于 iex-op 宏,它似乎做正确的事,而是使用 iex-op*,它没有:

Welcome to Racket 6.3
> (enter! "iex.rkt")
> (iex-op quote)
> iex-<TAB><TAB>
iex-base-url iex-op       iex-op*      iex-quote    iex-ver
> (iex-op* chart dividends)
> iex-<TAB><TAB>
iex-base-url    iex-dividends.0 iex-op*         iex-ver
iex-chart.0     iex-op          iex-quote

op* 定义的函数以.0 为后缀。我不知道为什么,尽管搜索了几个小时,我还是无法方便地找到有关它的文档。

当我在 DrRacket 中运行宏扩展器时,我发现 (iex-op* chart dividends) 实际上确实扩展为

(begin (iex-op chart) (iex-op dividends))

根据需要。更糟糕的是,当我在 REPL 中重现语法转换的结果时,它可以工作!

> (begin (iex-op chart) (iex-op dividends))
> iex-<TAB><TAB>
iex-base-url    iex-chart.0     iex-dividends.0 iex-op*         iex-ver
iex-chart       iex-dividends   iex-op          iex-quote

我错过了什么?我会欣然承认我的代码可能需要一些实质性的清理(我正在慢慢地弯曲我的 Python/C/etc. 思维),但我不太关心它的美感,而更关心是什么奥秘导致了这种行为。

【问题讨论】:

    标签: macros racket


    【解决方案1】:

    .0 后缀实际上并不是生成绑定名称的后缀——它表示标识符是由宏引入的,并且存在于宏范围内。根本问题是你使用了format-id

    请记住,Racket 的宏系统是卫生的。这意味着宏引入的绑定只能由宏看到。这使得 REPL 中的自动完成有点复杂,我不认为 REPL 在这里所做的确实是一个非常有用的选择。也就是说,您可能想知道的是如何修复您的代码,而不是 REPL 显示带有 .0 后缀的宏引入绑定的复杂性。

    当您调用format-id 时,生成的标识符将具有与第一个参数相同的词法上下文。您可以认为这本质上意味着生成的标识符将与该句法处于相同的词法范围内。在您的情况下,您提供的是 stx,它代表宏的整个输入表单。

    当您直接使用iex-op 时,例如通过编写(iex-op quote),那么quote 标识符来自与包含(iex-op ....) 形式相同的词法范围。因此,当您调用format-id 并将(iex-op ....) 形式作为第一个参数时,您仍然会得到一个与quote 标识符共享相同词法范围的标识符,一切都很好。

    然而,当您使用 iex-op* 宏时,它会传递 op0 语法对象,但周围的 (iex-op ....) 形式来自 iex-op* 宏时间>。这意味着stx 现在是指iex-op* 宏内部的范围,不是op0 标识符的范围。要解决此问题,请将您的调用更改为 format-id,以便它创建一个与操作标识符具有相同范围的标识符,而不是周围的语法对象:

    (format-id #'oper "iex-~a" #'oper)
    

    现在您的宏将按预期工作。

    在完成之前,问问自己:这里有什么要点?以下是一对:

    1. 破坏卫生是微妙的。当你决定编写一个不卫生的宏(并且format-id 是不卫生的)时,请仔细考虑从哪里获取词汇上下文。考虑当用户在您的宏上编写宏时会发生什么。

      在这种情况下,您很幸运同时是 iex-opiex-op* 的作者,因此您可以在遇到问题后修复 iex-op。但如果你不是iex-op 的作者,只有iex-op*,你的处境会更加棘手。每当您编写任何不卫生的宏时,请考虑这些事情以避免此类问题。

    2. 根据经验,当不卫生地从另一个标识符伪造一个新标识符时,最好使用另一个标识符作为词法上下文的来源。这样,用户提供的标识符的词法范围将被保留,这是用户所期望的。

    可以说,Fear of Macros 中使用format-id 的一些示例在这方面是草率的。可能值得尝试改进它们以树立更好的榜样。

    【讨论】:

    • 太棒了,谢谢!正如你所猜到的,Fear of Macros 确实告诉了我如何格式化标识符,但我不明白范围。我的直觉是范围问题,但我错误地怀疑这将是转换阶段的问题。非常感谢!
    • 我已经好几年没有更新 FoM 了。今天我完成了一系列修复工作,包括将lctx arg 的示例/解释改进为format-iddatum-&gt;syntax。实时网站已更新。
    猜你喜欢
    • 2014-12-23
    • 1970-01-01
    • 1970-01-01
    • 2011-02-22
    • 1970-01-01
    • 2012-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多