【问题标题】:unbound identifier and no #%top syntax transformer未绑定标识符且没有 #%top 语法转换器
【发布时间】:2013-10-03 00:13:19
【问题描述】:

我正在玩一个流畅的界面(对于 fluent 的一个非常奇怪的定义),以便我可以更好地了解球拍宏,所以我模拟了这段代码来玩。

#lang racket
(require racket/syntax)
(struct Person (name location))

(define-syntax what
  (syntax-rules (is)
    ((what is x thing) (what is x's thing))

    ((what is x quote-s thing)
     (let* ((x-str (~a (quote x)))
            (x-id (format-id #'x-id "~a" x-str))
            (thing-str (~a (quote thing)))
            (thing-proper-str (cadr (regexp-match #rx"(.*)\\?" thing-str)))
            (thing-id (format-id #'thing-id "Person-~a" thing-proper-str)))
       ((eval thing-id) (eval (syntax-e x-id)))))))

(define John (Person "John Luser" "Here"))
(what is John's name?)

运行此命令会生成 John: unbound identifier; also, no #%top syntax transformer is bound in: John。如果我不得不猜测,我会说我用标识符和字符串做的所有事情都会剥夺 John 的绑定符号,或者我在错误的环境中进行评估,但我不知道如何修复其中。

【问题讨论】:

    标签: macros racket


    【解决方案1】:

    只是为了比较:如果您将大部分工作推到编译时阶段,您的宏会是什么样子:

    #lang racket
    (require (for-syntax racket/format
                         racket/syntax))
    
    (struct Person (name location))
    
    (define-syntax (what stx)
      (syntax-case stx (is)
        ((what is x thing) 
         #'(what is x's thing))
    
        ((what is x quote-s thing)
         (let* ((thing-str (~a (syntax-e #'thing)))
                (thing-proper-str (cadr (regexp-match #rx"(.*)\\?" thing-str)))
                (thing-id (format-id stx "Person-~a" thing-proper-str)))
           #`(#,thing-id x)))))
    
    (define John (Person "John Luser" "Here"))
    (what is John's name?)
    

    需要注意的一点是,我们正在使用(require (for-syntax ...)),因为我们在编译时使用这些库,在程序源代码的预处理期间。

    另外,请注意,它更简单一些,因为我们并没有真正对正在执行的 x 进行任何修改,因此我们可以将那段语法保留在输出的语法中。


    如果有帮助,请考虑一下您在其他语言方面的经验。如果您熟悉 Java,请考虑如果您在程序的某处编写表达式:"hello " + "world",然后编译程序会发生什么。您是否希望 Java 编译器生成在执行时构造一个 "hello " 字符串、一个 "world" 字符串以及两者之间的字符串连接的字节码?大多数人会说“不,编译器应该将其重写为 "hello world" 文字作为编译的一部分”。

    这是预处理。当您在 Racket 中编写宏时,您就是在教 Racket 更多这些预处理规则。

    在这里,让我们尝试以下操作:我们将添加一个知道如何连接字符串的cat 命令。也就是说,让我们将这个串联示例变为现实。

    这是第一个版本:

    #lang racket
    
    (define (cat x y)
      (string-append x y))
    
    (define msg-head "hello ")
    (define msg-tail " world")
    
    (cat msg-head msg-tail)    
    (cat "hiya " "world")
    

    现在,我们知道,作为一个常规函数,这里cat 的两种用法最终都归结为对string-append 函数的调用。编译器不够聪明,不知道如何触摸cat 的第二次使用。

    但是如果我们想做同样的编译器重写规则怎么办:如果编译器预先看到源代码正在尝试cat 两个字符串文字,为什么不在编译时这样做,以便发出的字节码更好?

    好吧,让我们这样做。版本二如下:

    #lang racket
    
    (define-syntax (cat stx)
      (syntax-case stx ()
        [(_ x y)
         ;; At this point, the following code is being run by the _compiler_.
         (cond
           ;; If in the source code we're transforming, both x and y are
           ;; strings, we can do the concatenation at _compile time_.
           [(and (string? (syntax-e #'x))
                 (string? (syntax-e #'y)))
            (let ([xy
                   (string-append (syntax-e #'x) (syntax-e #'y))])
              ;; Once we have this string, we still need to emit it back as the
              ;; result of the rewrite.  We want to produce a piece of _syntax_
              ;; in place of the original stx.
              (datum->syntax stx xy))]
    
           [else
            ;; Otherwise, we want to produce a piece of syntax that looks like a
            ;; plain function call to string-append.
            (datum->syntax stx (list 'string-append #'x #'y))])]))
    
    (define msg-head "hello ")
    (define msg-tail " world")
    (cat msg-head msg-tail)
    (cat "hiya " "world")
    

    现在这种转换的问题在于观察:如果我们做对了,应该没有人能分辨出其中的区别! :P 否则,这将是一个破碎的转变。因此,要更轻松地查看差异,请按 DrRacket 工具栏上的 Macro Stepper 按钮:它会调用编译器,但会显示您的程序在转换为字节码之前发生的转换。

    【讨论】:

    • 我希望我能说这是有道理的,但我对引用、取消引用、准引用等如何工作的理解非常薄弱。直到最后一行我都可以看到你在做什么(我应该这样做,考虑到我自己做了很多事情),但我不明白为什么这段代码在编译时会被转换,而我的代码是运行时,也不完全是 #`(#,thing-id x) 的工作原理
    • docs.racket-lang.org/continue/#(part._.Rendering_.H.T.M.L) 简要介绍了常规列表的准引用。将它们视为一种非常轻量级的字符串模板语言功能,除了它可以处理结构而不是扁平字符串。请参阅docs.racket-lang.org/guide/qq.html 以获得更全面的指南。
    • 在原始示例上运行宏步进器。您应该立即看到有什么区别。我使用syntax-case 而不是syntax-rules 是有原因的:我希望能够在编译时进行计算,而syntax-case 让我这样做。 syntax-rules 更像是“用右侧替换左侧”的模式匹配之类的东西:你不能用 syntax-rules 进行任何重要的计算。
    【解决方案2】:

    哇,我是不是觉得很傻!通过将 ((eval thing-id) (eval (syntax-e x-id))) 更改为 ((eval thing-id) (eval x-id)) 发布此问题几分钟后修复它

    【讨论】:

    • 注意:你现在拥有宏的方式,所有的计算都是在运行时完成的。您通常希望在适当的地方尽可能多地推送到编译时。将术语从(what is John's name) 重写为(Person-name John) 可以在编译时完成。您需要查看 Greg Hendershott 的 Fear of Macros 等教程:greghendershott.com/fear-of-macros
    • eval 方法也很脆弱,因为它依赖于在您不一定能很好控制的环境下进行动态评估。我认为在 DrRacket 与文本 REPL 与独立程序中运行此程序时,您会得到不同的结果。它也没有利用你可以在 Racket 中做的很酷的编译时东西,所以我建议继续研究这部分:尽量避免eval,除非你没有其他选择。
    • @dyoo 不幸的是,元编程可能非常复杂,以至于我什至不知道我的其他选择是什么,更不用说它们如何工作以及如何使用它们了!
    • 你读过 Greg Hendershott 的 Fear of Macros 教程吗?它可能有助于阐明 Racket 宏系统为我们提供的特定元编程品牌。 greghendershott.com/fear-of-macros。挺好的!
    猜你喜欢
    • 2014-01-13
    • 2013-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-27
    • 1970-01-01
    • 1970-01-01
    • 2020-01-18
    相关资源
    最近更新 更多