只是为了比较:如果您将大部分工作推到编译时阶段,您的宏会是什么样子:
#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 按钮:它会调用编译器,但会显示您的程序在转换为字节码之前发生的转换。