15.2 Manipulating Namespaces
namespace 封装了两部分信息:
标识符到 binding 的映射。例如,namespace 可能将标识符 lambda 映射到 lambda 形式。"空"namespace 将每个标识符映射到未初始化的 top-level variable。
module name 到 module 声明和实例的映射。 (声明和实例之间的区别在 Module Instantiations and Visits 中讨论。)
第一种映射用于在 top-level 上下文中求值表达式,如 (eval '(lambda (x) (+ x 1)))。第二种映射被用于,例如, dynamic-require 定位 module。调用 (eval '(require racket/base)) 通常使用两部分:标识符映射确定 require 的 binding; 如果它的含义是 require,则使用 module 映射定位 racket/base module。
从核心 Racket 运行时系统的角度来看,所有求值都是反射式的。 执行从一个包含少数 primitive module 的初始 namespace 开始, 并通过按命令行指定的或 REPL 中加载文件和 module 来进一步填充。 Top-level require 和 define 形式调整标识符映射, 而 module 声明(通常因 require 形式而按需加载)调整 module 映射。
15.2.1 Creating and Installing Namespaces
make-empty-namespace 函数创建一个新的空 namespace。 由于该 namespace 是真正的空,起初无法用于求值任何 top-level 表达式—— 甚至不能求值 (require racket)。特别是,
(parameterize ([current-namespace (make-empty-namespace)]) (namespace-require 'racket))
会失败,因为该 namespace 不包含构建 racket 所依赖的 primitive module。
为使 namespace 可用,必须从现有 namespace 中 attach 一些 module。 Attach module 会通过从现有 namespace 的映射中转态复制条目(module 及其所有导入项), 来调整 module name 到实例的映射。通常,attach 的不是 primitive module (其名称和组织随时可能改变),而是更高级别的 module, 如 racket 或 racket/base。
make-base-empty-namespace 函数提供一个 namespace,它是空的, 但 attach 了 racket/base。结果 namespace 仍然"空", 即 namespace 的标识符到 binding 部分没有映射;只有 module 映射已被填充。 然而,有了初始 module 映射后,可以加载更多 module。
用 make-base-empty-namespace 创建的 namespace 适用于许多基本动态任务。 例如,假设 my-dsl 库实现了一种领域特定语言,你希望执行 来自用户指定文件的命令。用 make-base-empty-namespace 创建的 namespace 足以开始:
(define (run-dsl file) (parameterize ([current-namespace (make-base-empty-namespace)]) (namespace-require 'my-dsl) (load file)))
注意 current-namespace 的 parameterize 不影响标识符 (如 namespace-require)在 parameterize 主体内的含义。 这些标识符从其封闭上下文(很可能是 module)获得含义。 只有相对于此代码是动态的表达式(如 load 的文件内容) 才受 parameterize 影响。
上述示例中另一个细微之处在于使用了 (namespace-require 'my-dsl) 而不是 (eval '(require my-dsl))。后者不会生效,因为 eval 需要在 namespace 中获取 require 的含义,而 namespace 的 标识符映射起初是空的。namespace-require 函数则直接将给定 module 导入 current namespace。从 (namespace-require 'racket/base) 开始 会引入 require 的绑定,使得后续 (eval '(require my-dsl)) 可以工作。上述做法更好,不仅因为它更紧凑, 还因为它避免了引入不属于领域特定语言的绑定。
15.2.2 Sharing Data and Code Across Namespaces
未 attach 到新 namespace 的 module 在求值需要时会被重新加载和实例化。 例如,racket/base 不包含 racket/class, 再次加载 racket/class 会创建不同的 class 数据类型:
> (require racket/class) > (class? object%) #t
> (class? (parameterize ([current-namespace (make-base-empty-namespace)]) (namespace-require 'racket/class) ; loads again (eval 'object%))) #f
当动态加载的代码需要与其上下文共享更多代码和数据时,使用 namespace-attach-module 函数。namespace-attach-module 的第一个参数是从中获取 module 实例的源 namespace;在某些情况下, (current namespace 已知包含需要共享的 module:
> (require racket/class)
> (class? (let ([ns (make-base-empty-namespace)]) (namespace-attach-module (current-namespace) 'racket/class ns) (parameterize ([current-namespace ns]) (namespace-require 'racket/class) ; uses attached (eval 'object%)))) #t
然而在 module 内部,结合使用 define-namespace-anchor 和 namespace-anchor->empty-namespace 提供了获取源 namespace 的更可靠方法:
#lang racket/base (require racket/class) (define-namespace-anchor a) (define (load-plug-in file) (let ([ns (make-base-empty-namespace)]) (namespace-attach-module (namespace-anchor->empty-namespace a) 'racket/class ns) (parameterize ([current-namespace ns]) (dynamic-require file 'plug-in%))))
namespace-attach-module 绑定的锚将 module 的运行时 与加载 module 的 namespace(可能与 current namespace)连接起来。 在上述示例中,由于外层 module 需要 racket/class, namespace-anchor->empty-namespace 产生的 namespace 必然包含 racket/class 的实例。而且,该实例与导入 module 的实例相同, 因此 class 数据类型是共享的。