On this page:
15.1.1 Local Scopes
15.1.2 Namespaces
15.1.3 Namespaces and Modules

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

当然,如果我们只是想对 xy 给定值来求值表达式, 并不需要 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 来求值公式时, xy 的值并不可见:

> (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 无法看到 xy 的绑定, 恰恰因为它是一个 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 有时可与 environmentscope 互换使用。在 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)