Syntax对象是底层Racket编译器的词法上下文存储库。具体来说,当我们输入以下程序时:
#lang racket/base
(* 3 4)
编译器接收一个表示整个程序内容的语法对象。下面是一个示例,让我们看一下该语法对象的样子:
#lang racket/base
(define example-program
(open-input-string
"
#lang racket/base
(* 3 4)
"))
(read-accept-reader #t)
(define thingy (read-syntax 'the-test-program example-program))
(print thingy) (newline)
(syntax? thingy)
请注意,在程序中,
*
在
thingy
内部具有编译时表示为语法对象。目前,在
thingy
中的
*
还不知道它来自哪里:它还没有绑定信息。在编译期间的
扩展过程中,编译器将
*
与
#lang racket/base
中的
*
关联起来。
如果我们在编译时与这些内容进行交互,我们可以更轻松地看到这一点。(注意:我故意避免谈论
eval
,因为我想避免混淆关于编译时和运行时发生的事情的讨论。)
下面是一个示例,让我们更加了解这些语法对象的功能:
#lang racket/base
(require (for-syntax racket/base))
(define-syntax (at-compile-time stx)
(syntax-case stx ()
[(_ expr)
(let ()
(define the-expr #'expr)
(printf "I see the expression is: ~s\n" the-expr)
the-expr)]))
(at-compile-time (* 3 4))
我们将使用一个宏,在编译时,来让我们在编译期间检查事物的状态。如果您在DrRacket中运行此程序,您将看到DrRacket首先编译程序,然后运行它。编译程序时,当它遇到
at-compile-time
的用法时,编译器将调用我们的宏。
因此,在编译时,我们会看到类似这样的东西:
I see the expression is:
让我们稍微修改一下程序,看看是否可以检查标识符的identifier-binding
:
#lang racket/base
(require (for-syntax racket/base))
(define-syntax (at-compile-time stx)
(syntax-case stx ()
[(_ expr)
(let ()
(define the-expr #'expr)
(printf "I see the expression is: ~s\n" the-expr)
(when (identifier? the-expr)
(printf "The identifier binding is: ~s\n" (identifier-binding the-expr)))
the-expr)]))
((at-compile-time *) 3 4)
(let ([* +])
((at-compile-time *) 3 4))
如果我们在DrRacket中运行此程序,将会看到以下输出:
I see the expression is: #<syntax:21:18 *>
The identifier binding is: (#<module-path-index> * #<module-path-index> * 0 0 0)
I see the expression is: #<syntax:24:20 *>
The identifier binding is: lexical
12
7
(顺便说一句:为什么我们可以在编译时看到
at-compile-time
的输出呢?因为编译完全在运行时之前完成了!如果我们使用
raco make预编译程序并保存字节码,那么当我们运行程序时就不会看到编译器被调用。)
当编译器到达
at-compile-time
的使用时,它知道将适当的词法绑定信息与标识符关联起来。当我们检查第一个案例中的
identifier-binding
时,编译器知道它与特定模块相关联(在这种情况下,是
#lang racket/base
,这就是
module-path-index
的作用)。但在第二种情况下,它知道它是一个词法绑定:编译器已经遍历了
(let ([* +]) ...)
,所以它知道
*
的用法是指向由
let
设置的绑定。
Racket编译器使用语法对象将这种绑定信息传递给客户端,例如我们的宏。
尝试使用
eval
来检查这种东西存在许多问题:语法对象中的绑定信息可能不相关,因为在我们评估语法对象时,它们的绑定可能指向不存在的东西!这基本上就是你在实验中看到错误的原因。
尽管如此,这里有一个例子展示了s表达式和语法对象之间的区别:
#lang racket/base
(module mod1 racket/base
(provide x)
(define x #'(* 3 4)))
(module mod2 racket/base
(define * +)
(provide x)
(define x #'(* 3 4)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(require (prefix-in m1: (submod "." mod1))
(prefix-in m2: (submod "." mod2)))
(displayln m1:x)
(displayln (syntax->datum m1:x))
(eval m1:x)
(displayln m2:x)
(displayln (syntax->datum m2:x))
(eval m2:x)
这个例子被精心构造,以便语法对象的内容只涉及模块绑定的事物,这些事物将在我们使用eval
时存在。如果我们稍微改变一下这个例子,
(module broken-mod2 racket/base
(provide x)
(define x
(let ([* +])
#'(* 3 4))))
当我们尝试eval
从broken-mod2
中出来的x
时,事情就会出现严重的问题,因为语法对象正在引用一个在我们eval
之前不存在的词法绑定。 eval
是一种难以驾驭的东西。
(eval (shadow-stx))
应该返回6
。 - Matt