On this page:
15.2.1 Creating and Installing Namespaces
15.2.2 Sharing Data and Code Across Namespaces

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 requiredefine 形式调整标识符映射, 而 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, 如 racketracket/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-namespaceparameterize 不影响标识符 (如 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-anchornamespace-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/classnamespace-anchor->empty-namespace 产生的 namespace 必然包含 racket/class 的实例。而且,该实例与导入 module 的实例相同, 因此 class 数据类型是共享的。