在球拍宏扩展期间可以使用运行时信息吗?

时间:2015-12-23 20:20:07

标签: macros scheme racket

假设我在运行时有一个哈希表,其中包含字符串作为键。宏可以访问此信息并从中构建let表达式吗?

(define env (hash 'a 123 'b 321))

(magic-let env (+ a b)) ; 444

我知道我可以通过使用哈希表中的查找替换未定义的标识符来使用identifier-binding进行攻击,但是阴影将无法像普通let那样工作。

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

1 个答案:

答案 0 :(得分:7)

不,你不能这样做。至少不是你描述的方式。

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

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

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

这实际上是一件非常好的事情 - 它足够强大,足以支持非常复杂的系统,如果没有卫生就很难管理 - 但它会带来一些限制:

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

  2. 此外,范围规则更复杂,因为宏引入的绑定存在于它们自己的范围内。因此,绑定范围不一定是严格的词法。

  3. 您的magic-let无法完全按照您的描述工作,因为编译器无法静态推断出ab的绑定。但是,一切都不会丢失:可以挂钩到#%top,这是扩展器在遇到未绑定标识符时引入的神奇标识符。您可以使用它来使用哈希查找替换未绑定的值,并且可以使用syntax parameters在每个#%top内卫生地调整magic-let。这是一个例子:

    #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之类的情况下,可能有更好的方法来解决您的问题。如果你真的想要的话, 是可能的。