在球拍的宏扩展过程中可以使用运行时信息吗



假设我在运行时有一个以字符串为键的哈希表。宏是否可以访问这些信息并从中构建let表达式?

(define env (hash 'a 123 'b 321))
(magic-let env (+ a b)) ; 444

我知道我可以用identifier-binding破解,用哈希表中的查找替换未定义的标识符,但阴影将不会像在普通let中那样工作。

标记scheme也是因为我认为它的宏系统是相似的。

不,你不能那样做。至少不是你描述的那样。

无法在宏中访问运行时值的一般原因很简单:宏在编译时完全展开。编译程序时,运行时值根本不存在。一个程序可以被编译,字节码可以放在另一台计算机上,几周后它就会运行。宏观扩张已经发生。无论运行时发生什么,程序都不会更改。

由于多种原因,这一保证变得极其重要,但这对这个问题的讨论过于笼统。与讨论特定问题相关,这就是为什么绑定本身需要是静态的。

在Racket中,只要您在一个模块内(即不在顶层/REPL),所有绑定都可以在编译时静态解析。这在其他编程语言中是一个非常有用的属性,主要是因为编译器可以生成更高效的优化代码,但它在Racket或Scheme中尤其重要。这是因为宏系统的操作方式:在具有卫生宏的语言中,作用域是复杂的。

这实际上是一件非常好的事情——它足够强大,可以支持非常复杂的系统,如果没有卫生,这些系统将更难管理——但它引入了一些限制:

  1. 由于每个绑定都可以是宏运行时值,因此需要提前知道绑定才能执行程序扩展。编译器需要知道它是需要执行宏扩展还是仅仅发出变量引用。

  2. 此外,作用域规则要复杂得多,因为宏引入的绑定存在于它们自己的作用域中。正因为如此,绑定作用域不需要严格意义上的词法。

您的magic-let不能像您描述的那样工作,因为编译器不可能静态地推导出ab的绑定。然而,一切都没有失去:可以挂入#%top,这是扩展器在遇到未绑定的标识符时引入的一个神奇标识符。您可以使用它来用哈希查找替换未绑定的值,还可以使用语法参数在每个magic-let中卫生地调整#%top。这里有一个例子:

#lang racket
(require (rename-in racket/base [#%top base-#%top])
         racket/stxparam)
(define-syntax-parameter #%top (make-rename-transformer #'base-#%top))
(define-syntax-rule (magic-let env-expr expr ...)
  (let ([env env-expr])
    (syntax-parameterize ([#%top (syntax-rules ()
                                   [(_ . id) (hash-ref env 'id)])])
      (let () expr ...))))
(magic-let (hash 'a 123 'b 321) (+ a b)) ; => 444

当然,请记住,这将用哈希查找替换所有未绑定的标识符。其影响是双重的。首先,它不会遮蔽已经绑定的标识符:

(let ([a 1])
  (magic-let (hash 'a 2)
    a)) ; => 1

这可能是最好的,只是为了让事情保持半理智。这也意味着以下情况将引发运行时异常,而不是编译时错误:

(magic-let (hash 'a 123) (+ a b))
; hash-ref: no value found for key
;   key: 'b

我不建议这样做,因为这违背了Racket的很多哲学,而且可能会导致一些难以找到的错误。也许有一种更好的方法可以在不滥用#%top之类的东西的情况下解决你的问题。不过,如果你真的想要的话,是可能的。

最新更新