racket 宏展开时是否可以使用运行时信息?
Can runtime information be used during macro expansion in racket?
假设我在运行时有一个散列 table,它以字符串作为键。宏能否访问此信息并从中构建 let
表达式?
(define env (hash 'a 123 'b 321))
(magic-let env (+ a b)) ; 444
我知道我可以使用 identifier-binding
通过在散列 table 中查找替换未定义的标识符来破解 table 但是阴影将无法正常工作 let
.
也标记 scheme
因为我认为它的宏观系统是相似的。
不,你不能那样做。至少不是你描述的那样。
无法在宏中访问 运行time 值的一般原因很简单:宏在编译时完全展开。编译程序时,运行time 值根本不存在。一个程序可以编译,字节码可以放在另一台计算机上,这将 运行 几周后。宏观扩张已经发生。无论 运行 时间发生什么,程序都不会改变。
由于多种原因,这一保证被证明是非常重要的,但对于这个问题来说,这样的讨论过于笼统。它将与讨论特定问题相关,这就是为什么绑定本身需要是静态的。
在 Racket 中,只要你在模块中(即不在 top-level/REPL 中),所有绑定都可以在编译时静态解析。这在其他编程语言中非常有用属性,主要是因为编译器可以生成更高效的优化代码,但在 Racket 或 Scheme 中尤其重要。这是因为宏系统的运作方式:在具有卫生宏的语言中,范围是 复杂.
这实际上是一件好事——它足够健壮,可以支持非常复杂的系统,如果没有卫生,这些系统将难以管理——但它引入了一些限制:
因为每个绑定都可以是一个宏或一个运行时间值,所以需要提前知道绑定以便执行程序扩展。编译器需要知道它是否需要执行宏扩展或只是发出一个变量引用。
此外,范围规则要复杂得多,因为宏引入的绑定存在于它们自己的范围内。因此,绑定作用域不需要严格的词法。
您的 magic-let
无法像您描述的那样工作,因为编译器不可能静态地推断出 a
和 b
的绑定。然而,一切并没有丢失:你 可以 连接到 #%top
,这是扩展器在遇到未绑定标识符时引入的神奇标识符。您可以使用它通过散列查找替换未绑定的值,并且可以使用 syntax parameters 在每个 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
之类的东西。尽管如此, 还是有可能的,如果你真的想要的话。
假设我在运行时有一个散列 table,它以字符串作为键。宏能否访问此信息并从中构建 let
表达式?
(define env (hash 'a 123 'b 321))
(magic-let env (+ a b)) ; 444
我知道我可以使用 identifier-binding
通过在散列 table 中查找替换未定义的标识符来破解 table 但是阴影将无法正常工作 let
.
也标记 scheme
因为我认为它的宏观系统是相似的。
不,你不能那样做。至少不是你描述的那样。
无法在宏中访问 运行time 值的一般原因很简单:宏在编译时完全展开。编译程序时,运行time 值根本不存在。一个程序可以编译,字节码可以放在另一台计算机上,这将 运行 几周后。宏观扩张已经发生。无论 运行 时间发生什么,程序都不会改变。
由于多种原因,这一保证被证明是非常重要的,但对于这个问题来说,这样的讨论过于笼统。它将与讨论特定问题相关,这就是为什么绑定本身需要是静态的。
在 Racket 中,只要你在模块中(即不在 top-level/REPL 中),所有绑定都可以在编译时静态解析。这在其他编程语言中非常有用属性,主要是因为编译器可以生成更高效的优化代码,但在 Racket 或 Scheme 中尤其重要。这是因为宏系统的运作方式:在具有卫生宏的语言中,范围是 复杂.
这实际上是一件好事——它足够健壮,可以支持非常复杂的系统,如果没有卫生,这些系统将难以管理——但它引入了一些限制:
因为每个绑定都可以是一个宏或一个运行时间值,所以需要提前知道绑定以便执行程序扩展。编译器需要知道它是否需要执行宏扩展或只是发出一个变量引用。
此外,范围规则要复杂得多,因为宏引入的绑定存在于它们自己的范围内。因此,绑定作用域不需要严格的词法。
您的 magic-let
无法像您描述的那样工作,因为编译器不可能静态地推断出 a
和 b
的绑定。然而,一切并没有丢失:你 可以 连接到 #%top
,这是扩展器在遇到未绑定标识符时引入的神奇标识符。您可以使用它通过散列查找替换未绑定的值,并且可以使用 syntax parameters 在每个 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
之类的东西。尽管如此, 还是有可能的,如果你真的想要的话。