Racket 的 provide 为何报错,导出的宏为何出现未绑定标识符?
provide 报错是因为导出阶段与定义阶段不一致:define-syntax 在当前阶段建立语法绑定,而 (provide (for-syntax _RELEASED)) 要求导出高一个阶段的绑定。
代码中还有一个独立问题:(syntax-e #'_RELEASED) 得到的是符号 _RELEASED,不会读取它绑定的布尔值,所以这个条件始终为真。
第二个例子中的 (provide fn) 写法正确。在完整的 Racket 模块中,用普通 require 就能导入并使用这个宏。仅凭重启 DrRacket 后恢复正常,无法确定之前出现未绑定错误的原因。
为什么 define-syntax 与 for-syntax 对不上
这段代码涉及两个阶段:
- phase 0:模块的普通运行阶段。
- phase 1:展开该模块中的宏时,执行转换器代码的阶段。
这条定义:
(define-syntax _RELEASED #t)
在 phase 0 建立名为 _RELEASED 的语法绑定,右侧表达式则在 phase 1 求值。右侧在编译期求值,并不意味着 _RELEASED 是 phase 1 的普通变量。
这条导出声明:
(provide (for-syntax _RELEASED))
要求模块中存在 phase 1 的 _RELEASED 绑定。原代码没有定义这个绑定,因此报错:
provide: provided identifier is not defined or required
for-syntax 用于指定阶段。宏相关的名字并不都需要加上它,普通宏通常直接用 (provide 宏名) 导出。
将发布标志定义为编译期变量
如果 _RELEASED 只供 tc 的转换器读取,可以把它放进 begin-for-syntax,只导出 tc:
;; config.rkt
#lang racket/base
(require (for-syntax racket/base))
(provide tc)
(begin-for-syntax
(define _RELEASED #t))
(define-syntax (tc stx)
(syntax-case stx ()
[(_)
#'(void)]
[(_ body ...)
(if _RELEASED
#'(void)
#'(begin body ...))]))
begin-for-syntax 中的 define 建立了 phase 1 的普通变量。tc 的转换器也在这个阶段执行,因此可以直接读取 _RELEASED。
(require (for-syntax racket/base)) 引入转换器代码所需的编译期绑定。在 #lang racket/base 下,应显式写出这个依赖。
调用模块的代码如下:
;; main.rkt
#lang racket/base
(require "config.rkt")
(tc
(displayln "running tests")
(error 'test "test failed"))
(displayln "program started")
当 _RELEASED 为 #t 时,整个 tc 调用展开为:
(void)
测试代码不会进入后续的展开和执行过程。将 _RELEASED 改为 #f 后,带有测试内容的调用会展开为:
(begin
(displayln "running tests")
(error 'test "test failed"))
这时,测试代码会在程序运行时执行;编译期只决定保留还是丢弃这些代码。
如果其他模块也需要读取这个编译期配置,可以将前面的导出声明改为:
(provide tc
(for-syntax _RELEASED))
此时,begin-for-syntax 已将 _RELEASED 定义在 phase 1,导出阶段与定义阶段一致。如果它只供 tc 内部使用,就无须导出。
为什么 syntax-e 不能读取发布标志
原代码中的条件是:
(if (syntax-e #'_RELEASED)
#'(void)
#'(begin body ...))
#'_RELEASED 是包含标识符的语法对象。对它调用 syntax-e,得到的是符号:
'_RELEASED
syntax-e 不会查询标识符的绑定值,也不会对它求值。Racket 中只有 #f 被视为假,符号 _RELEASED 为真,所以这个分支始终选择 #'(void)。
因此,修复 provide 后,还需要修改这个条件。采用前面的编译期变量方案,直接读取 _RELEASED 即可:
(if _RELEASED
#'(void)
#'(begin body ...))
fn 宏可以直接通过 provide 导出
补上模块语言声明后,这两个文件就是一个完整示例:
;; common.rkt
#lang racket/base
(provide fn)
(define-syntax fn
(syntax-rules ()
[(_ . args)
(lambda . args)]))
;; a.rkt
#lang racket/base
(require "common.rkt")
((fn (x) (+ x 1)) 3)
其中的宏调用:
(fn (x) (+ x 1))
展开为:
(lambda (x) (+ x 1))
最后一个表达式的值为 4。
这里用普通 (provide fn) 和 (require "common.rkt") 即可。如果把导入改成:
(require (for-syntax "common.rkt"))
就会将 fn 的绑定移到高一个阶段,无法在示例中的普通表达式位置使用。
重启 DrRacket 后恢复,应该如何排查
题目没有提供出错时的完整文件、语言设置和操作过程,无法据此认定是缓存问题或 DrRacket 缺陷。重启后成功,也不能证明 provide 曾经失效。
可以按以下顺序排查:
-
确认两个文件都以预期的
#lang声明开头,并已保存到磁盘。 -
确认
"common.rkt"相对于a.rkt指向预期文件,避免加载另一个同名文件。 -
在 DrRacket 中对
a.rkt点击 Run,重新运行当前模块,不要只在旧的交互环境中执行最后一条表达式。 -
在这两个文件所在目录执行:
racket a.rkt
如果完整示例能在命令行运行,但某个 DrRacket 会话仍然报错,就需要检查该会话的语言设置、已加载的定义,以及实际运行的文件,才能判断具体原因。
发布标志决定宏展开的结果。修改 _RELEASED 后,使用 tc 的模块需要重新展开或编译,才能生成对应的代码;已经生成的代码不会在运行时自动切换测试模式。
备注:内容仅供参考。
