我正在尝试学习一些关于Racket及其宏系统的知识。我的介绍是围绕IEX stock API写了一个薄的包装器。我的预备阅读材料包括Greg Hendershott的Fear of Macros,Racket Guide(特别是关于宏生成宏和周围文档的部分)以及其他在线资源。
我希望能够做到这样的事情:
(iex-op* chart dividends quote)
生成函数iex-chart
,iex-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 /等等),但我不太关心它的美学,更多的是关于arcanum导致这种行为的原因。
答案 0 :(得分:3)
.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)
现在您的宏将按预期工作。
在结束之前,问问自己:这里有什么要点?这是一对夫妇:
打破卫生是微妙的。当你决定写一个不卫生的宏(format-id
是不卫生的)时,要非常认真地考虑从哪里获得词汇上下文。考虑当用户在宏上编写宏时会发生什么。
在这种情况下,您很幸运,您是iex-op
和iex-op*
的作者,因此您可以在遇到问题后修复iex-op
。但如果您不是iex-op
的作者,只有iex-op*
,那么您将处于一个更棘手的情况。每当你写任何不卫生的宏来避免这些问题时,请考虑这些事情。
根据经验,当从另一个标识符中不卫生地伪造新标识符时,最好使用该其他标识符作为词汇上下文的来源。这样,将保留用户提供的标识符的词法范围,这是用户期望的。
可以说,使用format-id
的恐惧宏中的一些例子在这方面很草率。可能值得尝试改进它们以设置更好的例子。