15 反射与动态执行

来源:互联网 发布:loveless world知乎 编辑:程序博客网 时间:2024/06/06 04:10

15.1 eval

eval 函数,接受一个表达式和定义(quoted形式或语法对象),计算其值:

> (eval '(+ 1 2))3

更强大的地方是,表达式可以动态构造:

> (define (eval-formula formula)    (eval `(let ([x 2]                 [y 3])             ,formula)))> (eval-formula '(+ x y ))5> (eval-formula '(+ (* x y) y))9

当然,如果只想计算的表达式,具有固定的值,不必使用eval.更直接的方法是使用第一类函数对象:

> (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常常被直接或间接的用于整个模块.例如,一个程序可能使用 dynamic-require载入一个模块,这实际上是通过包装eval来动态载入模块代码实现的.

15.1.1 本地作用域(local Scopes)

eval无法从被调用的上下文中,识别出本地绑定.例如,在let中调用eaal,计算未给定值的x和y的表达式:

> (define (broken-eval-formula formula)    (let ([x 2]          [y 3])      (eval formula)))> (broken-eval-formula '(+ x y))x: undefined;  cannot reference undefined identifier

eval无法正确看到x和y的值,因为它是一个函数, 并且Racket是一个词法作用域的语言. 想像一下,如果eval这样实现:

> (define (eval x)    (eval-expanded (mcro-expand x)))

eval-expand被调用时,x的最新绑定,就是要评估的表达式,而不是在broken-eval-formula中的let绑定.词法作用域阻止这种混和错误的行为,所以,阻止eval在调用它的上下文中看到本地绑定.

您可能会想像即使eval无法在broken-eval-formula中看到本地绑定,实际上也必须存在将x映射到2和y到3的数据结构,并且您希望获得该数据结构。 其实没有这样的数据结构存在; 编译器可以在编译时自由替换x的每一次使用,因此在运行时,x的本地绑定在任何具体的意义上都不存在。 即使变量不能通过常量折叠消除,通常也可以消除变量的名字,并且持有本地值的数据结构也不相似于名-值的映射.

15.1.2 命名空间

由于eval无法看到来自调用它的上下文的绑定,因此需要另一种机制来确定动态可用的绑定。 命名空间是封装可用于动态评估的绑定的第一类值。

// TODO

> (define x 3)> (eval 'x)3
#lang racket(eval '(cons 1 2))

// TODO

#lang racket(define ns (make-base-namespace))(eval '(cons 1 2) ns) ; works

make-base-namespace创建一个命名空间,并使用导出racket/base进行初始化. 稍后的Manipulating Namespaces 提供更多创建和配置命名空间的信息.

15.1.3 命名空间和模块

与let绑定一样,词法范围意味着eval不能自动查看调用它的模块的定义。 然而,与允许绑定不同的是,Racket提供了一种将模块反映到命名空间中的方法。

module->namespace函数使用引用的模块路径,并生成一个命名空间,用于评估表达式和定义,就像它们出现在模块体中一样:

> (module m racket/base   ;创建一个模块,含有一个绑定 `x`    (define x 11))> (require 'm)            ; 导入模块> (define ns           (module->namespace ''m)) ;; (quoted 'm) 'm是模块名> (eval 'x ns)> 11

module->namespace在模块外部是有用的,这时模块的全名是己知的(require ‘m, 全名就是 ‘m), 但是在模块内部,模块全名可能不知道,因为它可能取决于实际被载入时模块代码的位置.

在模块内部, 使用define-namespace-anchor给模块声明一个反射钩,使用namespace-anchor->namespace 卷入模块的命名空间:

#lang racket(define-namespace-anchor a)(define ns (namespace-anchor->namespace a)) ;; to real in 模块的命名空间(define x 1)(define y 2)(eval '(cons x y) ns); 产生 (1 . 2)

15.2 控制命名空间

一个命名空间,包含两方面信息:
- 从标识符到绑定的映射。 例如,命名空间可以将标识符lambda映射到lambda表单。 “空”命名空间是将每个标识符映射到未初始化的顶级变量的命名空间。
- 从模块名称到模块声明和实例的映射。

第一种映射用于在顶级上下文中计算表达式, 像 (eval '(lambda (x) (+ x 1))).第二类的用处,如:使用dynamic-require定位模块.(eval '(require racket/base)两方面都用到了:标识符映射决定 require的绑定;如果它代表require的意思,那么 模块映射装会被用于定位 racket/base模块.

从Racket运行时系统核心的角度来看,所有的计算都是反射(reflective).运行始于初始化包含几个原始模块的初始命名空间,然后进一步通过命令行或REPL载入指定的文件和模块.顶层的 requiredefine调整标识符映射,和模块声明(通常按需加载)调整模块映射.

15.2.1 创建和安装命名空间

make-empty-namespace 创建一个新的,空的命名空间.因为命名空间是空的,所以它不能被用于执行顶层表达式–包括(require racket). 例如:

(parameterize ([current-namespace (make-empty-namespace)])  (namespace-require 'racket'))

将会失败,因为命名空间,没有包含构建Racket的原始模块.

要使一个命名空间可用, 必须从己存在的命名空间附加一些模块.通过从现有命名空间的映射中传递复制条目(模块及其所有导入),附加模块可将模块名称映射到实例。通常,不仅仅是附加原始模块–使它们的名字和组织被改变, 而是更高级的模块被附加,如racketracket/base,

make-base-empty-namespace 提供一个附加了racket/base的命名空间.标识符和绑定还没有被映射,从这个方面来看,命名空间仍然是空的,但是模块映射己经被附加了.便是,通过初始模块映射,可以载入更多模块.

使用make-base-empty-namespace创建的命名空间适用于许多基本的动态任务。 例如,假设my-dsl库实现了一个领域特定语言,您希望从用户指定的文件中执行命令。 使用make-base-empty-namespace创建的命名空间足以开始:

(define (run-dsl file)  (parameterize ([current-namespace (make-base-empty-namespace)])    (namespace-require 'my-dsl)    (load file)))

注意,在parameterize体中, current-namespaceparameterize不会影响namespace-require等标识符的含义.那些标识符从封闭的上下文(可能是一个模块)中获得它们的含义。只有动态的代码(expressions that are dynamic with respect to this code),如被载入文件的内容,才会受parameterize影响.

上面例子中另外一个微妙地方是使用(namespace-require 'my-dsl) 而没有使用(eval 'require my-dsl).后者不会工作,因为eval需要在命名空间中获取require的含义,并且命名空间的标识符映射被初始化为空.namespace-require的区别是,直接导入给定模块到当前命名空间. 使用(namespace-require 'racket/base)开始,将首次引入’require’的绑定,并使随后的(eval '(require my-dsl))可以工作.上述处理更好,不仅因为更紧凑,同时也避免引入领域特定语言无关的绑定.

15.2.2 在命名空间共亨数据和代码

// TODO