15.1 eval
此示例在模块内或 DrRacket 的定义窗口中无法运行, 但可以在交互窗口中运行,原因将在 Namespaces 末尾解释。
eval 函数接受表达式或定义的表示("quoted" 形式或 syntax object)并对其进行求值:
> (eval '(+ 1 2)) 3
eval 的强大之处在于可以动态构造表达式:
> (define (eval-formula formula) (eval `(let ([x 2] [y 3]) ,formula))) > (eval-formula '(+ x y)) 5
> (eval-formula '(+ (* x y) y)) 9
当然,如果我们只是想对 x 和 y 给定值来求值表达式, 并不需要 eval。更直接的方式是使用 first-class function:
> (define (apply-formula formula-proc) (formula-proc 2 3)) > (apply-formula (lambda (x y) (+ x y))) 5
> (apply-formula (lambda (x y) (+ (* x y) y))) 9
然而,如果 (+ x y) 和 (+ (* x y) y) 等表达式 来自用户提供的文件,则 eval 可能是合适的。类似地,REPL 读取用户输入的表达式并使用 eval 对其进行求值。
此外,eval 常常直接或间接作用于整个 module。例如, 程序可以使用 dynamic-require 按需加载一个 module, 它本质上是对 eval 的包装,用于动态加载 module 代码。
15.1.1 Local Scopes
eval 函数无法看到其调用上下文中的局部绑定。例如, 在未 quoted 的 let 形式内调用 eval 来求值公式时, x 和 y 的值并不可见:
> (define (broken-eval-formula formula) (let ([x 2] [y 3]) (eval formula))) > (broken-eval-formula '(+ x y)) x: undefined;
cannot reference an identifier before its definition
in module: top-level
eval 无法看到 x 和 y 的绑定, 恰恰因为它是一个 function,而 Racket 是词法作用域语言。 假设 eval 实现如下
(define (eval x) (eval-expanded (macro-expand x)))
则在调用 eval-expanded 时,x 最近的绑定是待求值的表达式, 而不是 broken-eval-formula 中的 let 绑定。词法作用域防止了 这种混乱脆弱的行为,因此也阻止了 eval 看到其调用上下文中的局部绑定。
你可能会想,即使 eval 看不到 broken-eval-formula 中的局部绑定, 实际上必然存在将 x 映射到 2 且将 y 映射到 3 的数据结构,并希望获取该数据结构。实际上,该数据结构并不存在; 编译器可以在编译时自由地将所有 x 替换为 2, 因此 x 的局部绑定在运行时并不以任何具体形式存在。 即使变量无法通过 constant-folding 被消除,通常变量名也可以被消除, 保存局部值的数据结构并不像从名字到值的映射。
15.1.2 Namespaces
由于 eval 无法看到其调用上下文中的绑定,需要另一种机制来 确定动态可用的绑定。namespace 是一个 first-class value, 封装了可用于动态求值的绑定。
非正式地,namespace 有时可与 environment 或 scope 互换使用。在 Racket 中,namespace 具有更具体的、 上述的动态含义,不应与静态词法概念混淆。
某些函数(如 eval)接受可选的 namespace 参数。更常见的是, 动态操作使用的 namespace 是由 current-namespace parameter 确定的 current namespace。
在 REPL 中使用 eval 时,current namespace 是 REPL 用于求值表达式的 namespace。这就是为什么以下交互能通过 eval 成功访问 x:
> (define x 3) > (eval 'x) 3
相反,尝试以下简单 module,直接在 DrRacket 中运行它, 或将文件作为命令行参数传给 racket:
#lang racket (eval '(cons 1 2))
这会失败,因为初始 current namespace 是空的。当你以交互模式运行 racket(参见 Interactive Mode)时,初始 namespace 用 racket module 的导出项初始化,但当你直接运行 module 时, 初始 namespace 从零开始。
一般来说,使用当前已安装的 namespace 来调用 eval 不是好做法。 相反,应显式创建 namespace 并为 eval 调用安装它:
#lang racket (define ns (make-base-namespace)) (eval '(cons 1 2) ns) ; works
make-base-namespace 函数创建一个 namespace,用 racket/base 的导出项进行初始化。后面的 Manipulating Namespaces 一节提供了更多关于 创建和配置 namespace 的信息。
15.1.3 Namespaces and Modules
与 let 绑定一样,词法作用域意味着 eval 无法自动看到 其调用的 module 中的定义。但与 let 绑定不同的是, Racket 提供了将 module 反射到 namespace 的方法。
module->namespace 函数接受 quoted 的 module path, 并产生一个 namespace,用于求值表达式和定义,就像它们出现在 module 主体中一样:
> (module m racket/base (define x 11)) > (require 'm) > (define ns (module->namespace ''m)) > (eval 'x ns) 11
''m 中的双重 quoting 是因为 'm 是指向 交互式声明 module 的 module path,所以 ''m 是该 path 的 quoted 形式。
module->namespace 主要在 module 外部使用,此时 module 的全名已知。 然而在 module 形式内部,module 的全名可能未知, 因为它可能取决于 module 源文件加载时的位置。
在 module 内部,使用 define-namespace-anchor 在 module 上声明反射钩子,并使用 namespace-anchor->namespace 获取 module 的 namespace:
#lang racket (define-namespace-anchor a) (define ns (namespace-anchor->namespace a)) (define x 1) (define y 2) (eval '(cons x y) ns) ; produces (1 . 2)