Skip to content

Latest commit

 

History

History
630 lines (453 loc) · 36.2 KB

10.LibrariesAndTop-levelPrograms.md

File metadata and controls

630 lines (453 loc) · 36.2 KB

库和顶层程序

《Scheme语言修订6报告》描述了两类可移植代码单元:库和顶层程序。库是一种关于绑定的具名集合,具有一组显式导出的绑定的声明,一组导入库的声明,以及一个用于初始化它的绑定的程序体。顶层程序程序是一种独立程序,具有一组已声明的导入库和一个在运行顶层程序时才会运行的主体。 只有在顶层程序直接或间接使用库时,才会创建库中的绑定并运行其初始化代码。

库和顶层程序中出现的 import 声明有两个用途:第一,加载被导入的库;第二,使得被导入库中的绑定在导入到的库或顶层程序中可见。库通常存储在文件系统中,每个文件有一个库,库名通常标识库的文件系统路径,可能相对于默认或程序员指定的库位置集。运行顶层程序和加载库的确切机制取决于实现。

本章介绍了在 Chez Scheme 中加载库和程序的机制,以及用于控制和跟踪此过程的各种功能。它还描述了一组内置库和语法形式,用于定义库或顶层程序文件之外的新库和顶层程序。

10.1. 内置库

除了经《Scheme语言修订6报告》规定的 RNRS 库:

  (rnrs base (6))
  (rnrs arithmetic bitwise (6))
  (rnrs arithmetic fixnums (6))
  (rnrs arithmetic flonums (6))
  (rnrs bytevectors (6))
  (rnrs conditions (6))
  (rnrs control (6))
  (rnrs enums (6))
  (rnrs eval (6))
  (rnrs exceptions (6))
  (rnrs files (6))
  (rnrs hashtables (6))
  (rnrs io ports (6))
  (rnrs io simple (6))
  (rnrs lists (6))
  (rnrs mutable-pairs (6))
  (rnrs mutable-strings (6))
  (rnrs programs (6))
  (rnrs r5rs (6))
  (rnrs records procedural (6))
  (rnrs records syntactic (6))
  (rnrs records inspection (6))
  (rnrs sorting (6))
  (rnrs syntax-case (6))
  (rnrs unicode (6))

Chez Scheme 还提供了两个额外的库:(chezscheme)(chezscheme csv7)。前者也可以引用为 (scheme),后者也可以引用为 (scheme csv7)

本文档中描述了 chezscheme 库所导出的每个标识符绑定,包括像 lambda、辅助关键字 else、模块名 scheme 和过程名 cons。在大多数情况下,从 (chezscheme) 库导出的标识符对应于从 RNRS 其中一个库导出的标识符,绑定是相同的。然而,在某些情况下,(chezscheme) 绑定以某种方式扩展了 rnrs 绑定。例如,(chezscheme) syntax-rules 形式允许其子句具有防护板(第11.2节),而 (rnrs) syntax-rules 形式则不允许。类似地,(chezscheme) current-input-port 过程接受一个可选的端口参数,指定该参数后,会将当前输入端口设置为 port(第9.8节),而 (rnrs) current-input-port 过程则不接受该参数。当 (chezscheme) 库以某种方式扩展 RNRS 绑定时,(chezscheme) 库还导出 RNRS 版本,其名称前缀为 r6rs:,例如 r6rs:syntax-rulesr6rs:current-input-port

第7版 (chezscheme csv7) 向后兼容库包含一组语法形式和过程的绑定,这些语法或过程语义与 RNRS 中相同标志服的绑定直接冲突。以下标识符从 (chezscheme csv7) 导出。

record-field-accessible?
record-field-accessor
record-field-mutable?
record-field-mutator
record-type-descriptor
record-type-field-decls
record-type-field-names
record-type-name
record-type-symbol

这个库的绑定应仅用于旧代码;新代码应使用 RNRS 变体。这些标识符的每一个都可以在 (chezscheme) 库中带上前缀 csv7: 来使用,例如 csv7:record-type-name

作用于库或 RNRS 顶层程序范围之外的代码的交互环境包含 (chezscheme) 库的所有绑定,如第2.3节所述。

10.2. 运行顶层程序

顶层程序必须存在于其自己的文件中,该文件可以具有任何名字,并且可以保存在文件系统中的任何位置。存在于文件中的顶层程序由以下三种机制之一运行:scheme-script 命令,--program 命令行参数或 load-program 程序。

scheme-script 的使用方式如下:

scheme-script program-filename arg ...

它也可以在基于 Unix 的系统上隐式运行,方法是将下面这行代码

#! /usr/bin/env scheme-scrip

放在包含顶层程序的文件的最顶端,并且使顶层程序文件可执行,然后执行该文件。这一行也可以替换为:

#! /usr/bin/scheme-script

可以将 /usr/bin 替换为包含 scheme-script 的目录的绝对路径(如果它不在/usr/bin中)。 第一种形式在《Scheme语言修订6报告》[29]的非规范性附录中被推荐使用,并且在路径中出现 scheme-script 的任何地方都可以使用。

schemepetite 可执行文件来说,--program 命令的使用方式类似,通过运行:

scheme --program program-filename arg ...
petite --program program-filename arg ...

或者通过在顶层程序文件的最顶端包含以下代码:

#! /usr/bin/scheme --scrip

#! /usr/bin/petite --script

并使文件可执行来执行程序。再次强调,如果 scheme 或者 petite 不存在于 /usr/bin,那么 /usr/bin 就应该被替换为包含它们的实际目录的绝对路径。

在 12.4 节描述的 load-program 过程,其使用方式如同 load

(load-program string)

其中 string 代表了存储顶层程序的文件名。

无论使用何种机制,如果开头行是上述形式之一,或者更一般地说,由 #! 后跟空格或正斜杠组成,则开头行不被视为程序的一部分部分,一旦 Scheme 系统启动并开始运行程序,它将被忽略。因此,即使在由 load-program 加载的文件中,该行也可能存在。事实上,上述其他两种机制正是通过第12.8节中描述的 scheme-program 参数的值,最终调用 load-program ,并由 load-program 在程序求值之前扫描 #! 行(如果存在)。

可以使用第 12.4 节中描述的 compile-program 过程编译顶层程序。 compile-program#! 行从源文件复制到目标文件,后面接着源代码的编译版本。除了内置库之外,顶层程序所依赖的任何库都必须首先通过 compile-filecompile-library 进行编译。 这可以手动完成,也可以在编译程序之前将参数对象 compile-imported-libraries 设置为 #t 来实现。如果它所依赖的任何库被重新编译,则该程序也需要重新编译。通过上述各种机制,编译后的顶层程序可以像源文件顶层程序一样运行。

在 Chez Scheme 中,库也可以在 REPL 中定义,或者放在要通过 loadload-library 加载的文件中。无论库是放在自己的文件中还是通过 import 隐式地加载、键入到 REPL 中或者放在待通过 load 求值的、顶层表达式的单独文件中,其定义的语法都是相同的。顶层程序也可以在 REPL 中定义,或者放在要通过 load 加载的文件中,但在这种情况下,语法略有不同。在《Scheme语言修订6报告》定义的语言中,顶层程序仅仅是一个展开的形式序列,由 import 形式和主体组成,仅由其所在文件的边界分隔。为了在 REPL 中键入顶层程序或将其置于要通过 load 加载的文件中,Chez Scheme 允许将顶层程序包含在 top-level-program 形式中。

10.3 库和顶层程序形式

  • 语法:(library name exports imports library-body)
  • 返回:未定义
  • 所属库:(chezscheme)

library 形式定义了一个名称为 name 、导出表为 exports、导入表为 imports 以及程序体为 body 的新库。有关库形式语法和语义的详细信息,请参见《Scheme 程序设计语言(第4版)》的第10.3节以及《Scheme语言修订6报告》。

在任何给定时间里,只能加载库的一个版本,如果在已加载库的另一个版本后、通过 import 隐式地加载库,则会引发异常。Chez Scheme 允许将库的不同版本或相同版本的新实例显式键入到 REPL 中或从文件中显式加载,以便于交互式测试和调试。程序员应该注意确保使用该库的任何代码也被重新输入或重新加载,以确保代码访问的是库新实例的绑定。

(library (test (1)) (export x) (import (rnrs)) (define x 3))
(import (test))
(define f (lambda () x))
(f) ; ==> 3 

(library (test (1)) (export x) (import (rnrs)) (define x 4))
(import (test))
(f) ; ==> 3    ; oops---forgot to redefine f (define f (lambda () x))
(f) ; ==> 4 

(library (test (2)) (export x) (import (rnrs)) (define x 5))
(import (test))
(define f (lambda () x))
(f) ; ==> 5

与模块导入一样(第11.5节),库 import 也可能出现在定义可能出现的任何地方,包括在 REPL 的顶层、由 load 加载的文件,或者 lambdaletletrecletrec* 等的主体中。可以使用相同的 import 形式来导入库和模块。

(library (foo) (export a) (import (rnrs)) (define a 'a-from-foo))
(module bar (b) (define b 'b-from-bar))
(let () (import (foo) bar) (list a b)) ; => (a-from-foo b-from-bar)

除非库从 (chezscheme) 库中导入 import 关键字,否则它在库的主体中不可见。

  • 语法:(top-level-program imports body)
  • 返回:未定义
  • 所属库:(chezscheme)

top-level-program 形式可以键入到 REPL 中,也可以放在要通过 load 加载的文件中,其行为就好像它的子形式放在文件中并通过 load-program 加载一样。顶层程序语法和语义在《Scheme 程序设计语言(第4版)》的第10.3节以及《Scheme语言修订6报告》中有具体的描述。

下面的代码演示了在 REPL 中如何使用 top-level-program

> (top-level-program (import (rnrs))
    (display "hello!\n"))
hello!

10.4 独立的 importexport 形式

尽管《Scheme语言修订6报告》没有要求,但 Chez Scheme 支持使用独立的 importexport 形式。import 形式可以出现在其他定义可以出现的任何地方,包括 library 的主体、module(第 11.5 节)的主体、lambda 或其他局部主体以及程序顶层。export 形式可以出现在 librarymodule 主体的定义中,以指定 librarymodule 的其他导出。

在库或顶层程序中,这些形式的关键字必须从 (chezscheme) 库中导入才能使用,因为它们未在任何《Scheme语言修订6报告》中定义。

  • 语法:(import import-spec ...)
  • 语法:(import-only import-spec ...)
  • 返回:未定义
  • 所属库:(chezscheme)

importimport-only 形式是一条定义,可以出现在其他定义可以出现的任何地方,包括在程序的顶层、嵌套在 lambda 表达式的主体中以及嵌套在模块和库中。

每条 import-spec 必须采用以下形式之一:

import-set
(for import-set import-level ...)

《Scheme 程序设计语言(第4版)》的第10章描述了 for 包装语法和 import-level 。Chez Scheme 会忽略它们,它们会根据《Scheme语言修订6报告》允许自动确定必须导入的标识符的级别。这使程序员免于这样做的义务,并使编译和运行期实际导入的库集更通用和更精确 [21,19]。

import-sert 必须采用以下形式之一:

library-spec
module-name
(only import-set identifier ...)
(except import-set identifier ...)
(prefix import-set prefix)
(add-prefix import-set prefix)
(drop-prefix import-set prefix)
(rename import-set (import-name internal-name) ...)
(alias import-set (import-name internal-name) ...)

这些形式中有几个由《Scheme语言修订6报告》指定;其余的是 Chez Scheme 扩展,包括 module-nameadd-prefixdrop-prefix 以及 alias 形式。

importimport-only 形式使指定的绑定在它们出现的范围内可见。除了在顶层,两者的不同之处在于:除了被所导入的名字遮蔽起来的绑定,import 将所有绑定可见,而 import-only 则会隐藏所有已有绑定,即,仅使导入的名字可见。在顶层,import-onlyimport 具有相同的行为。

每一条 import-set 按下面的方式指明了要如何使名字可见:

library-spec
所有被《Scheme语言修订6报告》指明的库的导出
module-name
由标识符 module-name 指定的模块的所有导出
(only import-set identifier ...)
对于由 import-set 指定的导出,仅导出 identifier ...
(except import-set identifier ...)
identifier ... 以外的由 import-set 指定的所有标识符
(prefix import-set prefix)
所有由 import-set 指定的标识符并添加前缀 prefix
(add-prefix import-set prefix)
所有由 import-set 指定的标识符并添加前缀 prefix(类似于 prefix
(drop-prefix import-set prefix)
所有由 import-set 指定的标识符并移除前缀 prefix
(rename import-set (import-name internal-name) ...)
所有由 import-set 指定的标识符并会根据每对规则将 import-name 重命名为对应的标识符 internal-name
(alias import-set (import-name internal-name) ...)
所有由 import-set 指定的标识符并根据每对规则为 import-name 添加别名 internal-name

rename 形式的不同,alias 形式中的 import-nameinternal-name 都会存在于结果集合中,而不只是保留 internal-name

如果因为导出或前缀的缺失,导致给定的选取或变换无法完成,则会产生语法违规(Syntax Violation)

对通过导入模块或库而变得可见的标识符来说,它的作用域就好像它的定义出现在导入发生的地方一样。以下示例使用局部模块 m 说明了这些作用域规则。

(library (A) (export x) (import (rnrs)) (define x 0))
(let ([x 1])
  (module m (x setter)
    (define-syntax x (identifier-syntax z))
    (define setter (lambda (x) (set! z x)))
    (define z 2))
  (let ([y x] [z 3])
    (import m (prefix (A) a:))
    (setter 4)
    (list x a:x y z))) ;==> (4 0 1 3)

内部 let 表达式将 y 绑定到外部 let 绑定的 x 的值。 m 的导入使得 xsetter 的定义在内部 let 中可见。(A) 的导入使得从 (A) 导出的变量 x 在内部 let 的主体内以 a:x 的形式可见。因此,在表达式 (list x a:x y z) 中,x 指的是从 m 导出的标识符宏,而 a:x 指的是从 (A) 导出的变量 x,而 yz 指的是由内部 let 建立的绑定。标识符宏 x 扩展为对模块中定义的变量 z 的引用。

对于局部导入形式,很少需要使用扩展导入说明符。例如,封装了导入和引用的抽象可以很容易地定义,并按如下方式使用:

(define-syntax from
  (syntax-rules ()
    [(_ m id) (let () (import-only m) id)]))

(library (A) (export x) (import (rnrs)) (define x 1))
(let ([x 10])
  (module M (x) (define x 2))
  (cons (from (A) x) (from M x))) ; ==> (1 . 2)

可以不用 import-only 而用 import 来定义 from ,但是如果没有从库或模块中成功导出指定的标识符,使用 import-only 就会得到反馈。如果使用 import 而非 import-only,那么当库或模块并没有导出指定的名字时,当前的绑定可能会变得可见。

(define-syntax lax-from
  (syntax-rules ()
    [(_ m id) (let () (import m) id)]))

(library (A) (export x) (import (rnrs)) (define x 1))

(let ([x 10])
  (module M (x) (define x 2))
  (+ (from (A) x) (from M y))) ; ==> exception: unbound identifier y
 
(let ([x 10] [y 20])
  (module M (x) (define x 2))
  (+ (lax-from (A) x) (lax-from M y))) ; ==> 21

正如人们所期望的那样,导入的可见性与卫生宏展开是以这样一种方式相互作用的:从模块 M 导入的标识符 x 在导入上下文中被处理,就好像相应的导出标识符与 M 一起存在于导入形式中。

上面的 from 抽象是有效的,因为 Mid 都出现在抽象的输入中,所以导入的 id 捕获了对 id 的引用。

from 形式的以下变体也有效,因为所有的名字都是由转换器引入输出的。

(module M (x) (define x 'x-of-M))
(define-syntax x-from-M
  (syntax-rules ()
    [(_) (let () (import M) x)]))

另一方面,由 import 引入的模块名字不会捕获自由引用。

(let ([x 'local-x])
  (define-syntax alpha
    (syntax-rules ()
      [(_ var) (let () (import M) (list x var))]))
 
  (alpha x)) ; ==> (x-of-M local-x)

类似地,来自于模块中自由变量的导入,不会捕获对引入变量的引用。

(let ([x 'local-x])
  (define-syntax beta
    (syntax-rules ()
      [(_ m var) (let () (import m) (list x var))]))

  (beta M x)) ; ==> (local-x x-of-M)

通过扩展的 import 描述符 prefixrename 以及 alias 引入的前缀化(prefixed)、**易名的(renamed)以及别名的(aliased)**绑定同样符合这样的语义。

from 抽象适用于变量,但不适用于导出的关键字、记录名或模块名,因为输出是一个表达式,因此只能出现在表达式可出现的地方。在 import* 的以下定义中,泛化使用了此技术,它支持重命名导入绑定和选择性导入特定绑定——无需使用内置的 import 子形式来选取和重命名标识符。

(define-syntax import*
  (syntax-rules ()
    [(_ m) (begin)]
    [(_ m (new old))
     (module (new)
       (module (tmp)
         (import m)
         (alias tmp old))
       (alias new tmp))]
    [(_ m id) (module (id) (import m))]
    [(_ m spec0 spec1 ...)
     (begin (import* m spec0) (import* m spec1 ...))]))

为了选择性地从模块或库 m 中导入标识符,import* 形式扩展为一个匿名模块,该模块首先导入 m 的所有导出,然后仅重新导出选定的标识符。为了在导入时重命名,宏会扩展为匿名模块,该模块会导出绑定到名字的别名(第 11.10 节)。

如果输出将 new 的定义放在与 m 的导入相同的作用域内,那么只要 new 也出现在 m 的接口中,就会出现命名冲突。为了防止这种情况,输出将导入放置在嵌套的匿名模块中,并通过引入的标识符 tmp 的别名链接新旧模块。

宏通过递归地展开来处理多个导入规范。以下每个示例都将 cons 导入为 + 并将 + 导入为 cons,这可能不是一个好主意。

(let ()
  (import* scheme (+ cons) (cons +))
  (+ (cons 1 2) (cons 3 4))) ; ==> (3 . 7)

(let ()
  (import* (rnrs) (+ cons) (cons +))
  (+ (cons 1 2) (cons 3 4))) ; ==> (3 . 7)
  • 语法:(export export-spec ...)
  • 返回:未定义
  • 所属库:(chezscheme)

export 形式是一条定义,可以与其他定义一起出现在 librarymodule 的前面。export 形式出现在其他上下文中是一种语法错误,包括顶层、顶层程序或 lambda 主体的定义中。

每条 export-spec 必须采用以下形式之一:

identifier
(rename (internal-name export-name) ...)
(import import-spec ...)

其中每个 internal-nameexport-name 都是一个标识符。前两种在语法上与 library export-spec 相同,而第三中在语法上与 Chez Scheme import 形式相同,后者是 R6RS 库 import 自形式的扩展。第一种形式以 identifier 命名单个导出,其导出名字与其内部名字相同。第二个命名一组导出,每个导出名字都明确给出并且可能与其内部名字不同。

对于第三种,由 import 形式指定的标识符都成为了导出,可以通过 import-spec 来指定其别名、重命名、前缀等。对于那些其中的绑定由一条出现在 export 形式中的 import 形式导出的库或模块,可以在导出模块和库的内部或外部定义,并且不需要在导出模块或库中的其他地方导入。

下面的库导出了 if 的一种 two-armed-only 变体,以及 (rnrs) 库的其余所有绑定。

(library (rnrs-no-one-armed-if) (export) (import (except (chezscheme) if))
  (export if (import (except (rnrs) if)))
  (define-syntax if
    (let ()
      (import (only (rnrs) if))
      (syntax-rules ()
        [(_ tst thn els) (if tst thn els)]))))

(import (rnrs-no-one-armed-if))
(if #t 3 4) ; ==> 3
(if #t 3) ; ==> exception: invalid syntax

定义相同库的另一种方式则是使用一个不同的内部名字来定义 two-armed-only-if,并在导出时使用 rename 将其重命名为 if

(library (rnrs-no-one-armed-if) (export) (import (chezscheme))
  (export (rename (two-armed-if if)) (import (except (rnrs) if)))
  (define-syntax two-armed-if
    (syntax-rules ()
      [(_ tst thn els) (if tst thn els)])))

(import (rnrs-no-one-armed-if))
(if #t 3 4) ; ==> 3
(if #t 3) ; ==> exception: invalid syntax

export 形式在库语句体中的位置无关紧要,例如,export 形式可以出现在上述例子中定义之后的任何地方。

  • 语法:(indirect-export id indirect-id ...)
  • 返回:未定义
  • 所属库:(chezscheme)

本形式是一条定义,可以出现在其他定义可出现的任何地方。

indirect-export 形式表明当 id 被导出到顶层时,由 indirect-id 指明的那些标识符也被间接地导出到顶层。

通常来说,如果标识符没有被库或模块直接导出,那么只可能由展开在该库或模块中定义且被导出的宏时,从外部引用该标识符。尽管如此,这也不可能发生在定义在顶层的库或模块(或嵌套在其他库或模块)中,除非 (1) 库或模块已设置为将所有标识符隐式导出为间接导出,或 (2) 每个被间接导出的标识符都被显式声明为某个其他标识符的间接导出,这些标识符通过 indirect-exportmodule 导出自形式中内置的间接导出功能从库或模块中直接或间接导出。默认情况下,(1) 对库为真,对模块为假,但可以通过 implicit-exports 形式覆盖默认值,如下所述。

这种形式仅在顶层库、顶层模块或包含在库或顶层模块中的模块中才有意义,尽管如此,如果库或模块已经隐式导出所有绑定,则它没有效果。 然而,它被允许出现在任何其他定义可以出现的地方,因此可以在任何定义上下文中使用展开为间接导出形式的宏。

间接导出的标识符罗列起来,使得编译器可以确定必须插入顶层环境的确切绑定集合(直接和间接),相反,可以更有效地将绑定集视为局部绑定(并且如果它们没有被使用,可能被丢弃)。

在下面的示例中,当 current-count 导出到顶层时,indirect-export 用于将 count 间接导出到顶层。

(module M (bump-count current-count)
  (define-syntax current-count (identifier-syntax count))
  (indirect-export current-count count)
  (define count 0)
  (define bump-count
    (lambda ()
      (set! count (+ count 1)))))

(import M)
(bump-count)
current-count ; ==> 1
count ; ==> exception: unbound identifier count

indirect-export 形式不要求 countbump-count 可见,因为它是一个过程,其代码包含在模块中,它不是一个宏,它可能在模块外部的某处展开为对 count 的引用。

如果 a 展开为对一个可能不是被直接导出的标识符的引用,那么在宏的输出中使用 indirect-export 就会非常有用,下面演示了上述模块 M 的替代定义:

(define-syntax define-counter
  (syntax-rules ()
    [(_ getter bumper init incr)
     (begin
       (define count init)
       (define-syntax getter (identifier-syntax count))
       (indirect-export getter count)
       (define bumper
         (lambda ()
           (set! count (incr count)))))]))

(module M (bump-count current-count)
  (define-counter current-count bump-count 0 add1))
  • 语法:(implicit-exports #t)
  • 语法:(implicit-exports #f)
  • 返回:未定义
  • 所属库:(chezscheme)

implicit-exports 形式是一个定义,可以与其他定义一起在 librarymodule 之前出现。implicit-exports 形式出现在其他上下文中则是语法错误,包括在顶层或顶层程序的定义或 lambda 表达式的主体中。

implicit-exports 形式决定,如果库或模块中的任意元绑定(关键字、元定义或属性定义)被直接导出到最高层,是否要自动间接地将一个不是从该模块或库直接导出的标识符导出到最高层。该形式的默认是值,对库来说是 #t ,而对于模块来说是 #f,以匹配《Scheme语言修订6报告》的要求。implicit-exports 形式仅在库、顶层模块或包含在库或顶层模块中的模块中有意义。它允许出现在 lambdalet 或类似主体中包含的模块中,但在那里被忽略,因为该模块的绑定都不能导出到顶层。

(implicit-exports #t) 的优点是间接导出不需要显式列出,这很方便。一个缺点是它通常会导致更多的绑定被提升到顶层,在这种情况下它们不能被优化器视为无用而丢弃。对于模块,另一个缺点是不能证明这种绑定是不可变的,这会抑制如过程内联这样的重要的优化。这个缺点导致运行时性能显着降低。

10.5. 显式调用库

  • 过程:(invoke-library libref)
  • 返回:未定义
  • 所属库:(chezscheme)

libref 必须是库引用形式的 S-表达式。库引用的语法在 《Scheme 程序设计语言(第4版) 的第 10 章和《Scheme语言修订6报告》中给出。

库会在库外某条表达式(比如在另一个库或顶层程序中)对库导出变量之一的引用求值时或求值之前被隐式地调用。当库被调用时,它的表达式主体(库中变量定义及其初始化表达式的右侧)会被调用。一旦被调用,除非它是首次显式地重新定义或重新加载,否则该库就不会在同一个进程中再次被调用。

invoke-library 显式调用 libref 指定的库,如果它尚未被调用或调用后又被重新定义或重新加载。如果库尚未加载,则 invoke-library 首先通过第 2.4 节中描述的过程加载库。

invoke-library 通常只对那些具有副作用的库有用。这在控制副作用何时发生并强制调用没有导出变量的库时很有用。调用库不会强制加载或求值编译期代码(宏转换器表达式和元定义),也不会导致库的绑定变得可见。

避免在库主体中出现外部可见的副作用是一种很好的做法,这样可以很好地使库在编译期和运行期被同等使用。 如果可行,请考虑将库主体的副作用移至初始化例程,并添加一个顶层程序用于导入库并调用初始化例程。使用这种结构,对库的 invoke-library 的调用可以被对顶层程序的 load-program 的调用所替代。

10.6 库参数

下面的参数描述了,当试图去加载一个库时,应该从何处查找导入、是否编译所加载的库、进行搜寻时是否显示跟踪消息。

  • 线程级参数:library-directories
  • 线程级参数:library-extensions
  • 所属库:(chezscheme)

参数 library-directories 确定包含库源代码和目标代码的文件在文件系统中的位置,参数 library-extensions 确定包含代码的文件的文件扩展名,如第 2.4 节所述。两个参数的值都是字符串序对组成的表。每对 library-directories 中的第一个字符串标识源文件根目录,第二个字符串标识相应的目标文件根目录。类似地,每个 library-extensions 对中的第一个字符串标识源文件扩展名,第二个字符串标识相应的目标文件扩展名。库源文件或目标文件的完整路径由源文件或目标文件根后跟以斜杠为前缀的库名称组成部分组成,并在末尾添加了库扩展名。例如,对于根目录 /usr/lib/scheme、库名称 (app lib1) 和扩展名 .sls,完整路径为 /usr/lib/scheme/app/lib1.sls 。如果库名部分形成绝对路径名,例如 ~/.myappinit ,则忽略 library-directories 参数并且不添加前缀。

这些参数的初始值如下所示:

(library-directories) ; ==>  (("." . "."))

(library-extensions) ; ==> ((".chezscheme.sls" . ".chezscheme.so")
                     ;      (".ss" . ".so")
                     ;      (".sls" . ".so")
                     ;      (".scm" . ".so")
                     ;      (".sch" . ".so"))

为方便起见,当设置了这些参数中的任何一个时,表中的任何元素都可以指定为单个源字符串,在这种情况下,将自动确定对象字符串。对于 library-directories ,目标字符串与源字符串相同,有效地将同一目录命名为源代码和目标代码根目录。 对于 library-extension,对象字符串是从字符串中删除最后一个(或唯一)扩展并附加 ".so" 的结果。 library-directorieslibrary-extensions 参数也接受第 2.5 节中描述的格式的输入字符串作为 --libdirs--libexts 命令行选项。

  • 线程级参数:compile-imported-library
  • 所属库:(chezscheme)

当此参数的值为 #t 时,对于那些目标文件缺失、目标文件编译时间早于源文件或通过 include 包含进来的任意源文件、或者本身需要一个库被重新编译(如2.4节所述)的库,import 在导入它们时会自动调用 compile-library-handler 参数对应的值(默认是一个简单地调用 compile-library 的过程)该参数的默认初始值为 #f。 它可以通过命令行选项 --compile-imported-libraries 设置为 #t

import 通过此机制编译库时,它不会同时加载已编译的库,因为这会导致库的某些部分被重新求值。正因如此,该文件中 library 形式之外的运行期表达式不会被求值。如果存在此类表达式并需要对其求值,则应显式加载该库。

  • 线程级参数:import-notify
  • 所属库:(chezscheme)

当新参数 import-notify 设置为真值时,import 会在搜索包含需要加载的每个库的文件时向控制台输出端口显示消息。该参数的默认值为 #f

  • 线程级参数:library-search-handler
  • 所属库:(chezscheme)

参数的值必须是一个过程,并且该过程遵循下面为default-library-search-handler 描述的协议,这也是该参数的默认值。

在导入、编译整个程序或编译整个库期间调用此参数的值来定位库的源代码或目标代码。

importcompile-whole-program 以及 compile-whole-library 过程在处理程序库期间,会调用该参数的值来定位源文件或目标代码。

  • 过程:(default-library-search-handler who library directories extensions)
  • 返回值:见下
  • 所属库:(chezscheme)

此过程是 library-search-handler 的默认值,会在 importcompile-whole-programcompile-whole-library 期间调用以定位库的源代码或目标代码。 who 是在 import-notify 消息中提供上下文的符号。 library 是所需库的名称。directories 是一张由 library-directories 返回形式的组织起来的源目录和对象目录序对组成的表。extensions 是以 library-extensions 返回的形式的源和对象扩展名序对组成的表。

此过程搜索指定的目录,直到找到具有指定扩展名之一的库源文件或目标文件。如果首先找到源文件,则构造相应的目标文件路径并检查该文件是否存在。如果它首先找到目标文件,则该过程在与该目标目录配对的源目录中查找具有给定源扩展名之一的相应源文件。该过程返回三个值:库源文件的文件系统路径或 #f(如果未找到)、相应目标文件的文件系统路径(可能是 #f)以及一个布尔值用来表示该目标文件是否存在。

10.7 审查库

  • 过程:(library-list)
  • 返回:一组由当前定义的库所组成的表
  • 所属库:(chezscheme)

最初定义的库集包括上面第 10.1 节中列出的库。

  • 过程:(library-version libref)
  • 返回:指定库的版本
  • 过程:(library-exports libref)
  • 返回:由指定库中的导出组成的表
  • 过程:(library-requirements libref)
  • 返回:由指定库所依赖的库组成的表
  • 过程:(library-requirements libref options)
  • 返回:由指定库所依赖的库组成的表,依据 options 过滤
  • 过程:(library-object-filename libref)
  • 返回:返回指定库对应目标文件的文件名,如果有的话
  • 所属库:(chezscheme)

只能获取内置的或先前加载到系统中的库的信息。libref 必须是库引用形式的 S-表达式。库引用的语法在《Scheme 程序设计语言(第四版)》的第 10 章和《Scheme语言修订6报告》中给出。

library-version 返回值是代表库版本的数字表(可能为空)。

library-exports 返回的导出表是一张由符号组成的表,每个符号都对应了库的一个导出。元素出现的顺序是未指定的。

当提供可选的 options 参数时,它必须有有效的库要求选项的符号上的枚举集,如下面的 library-requirements-options 条目中所述。它默认为包含所有选项的集合。library-requirements 返回的库列表的每个元素都是库引用的 S 表达式形式。库引用包括系统中存在的库的实际版本(如果非空),即使在导入时未指定版本。库在 library-requirements 返回的列表中出现的顺序未指定。

如果指定的库是从目标文件加载或编译到目标文件,则 library-object-filename 返回一个字符串表示目标文件的名字。否则,它返回#f

(with-output-to-file "A.ss"
  (lambda ()
    (pretty-print
      '(library (A (1 2)) (export x z)
         (import (rnrs))
         (define x 'ex)
         (define y 23)
         (define-syntax z
           (syntax-rules ()
             [(_ e) (+ y e)])))))
  'replace)
(with-output-to-file "B.ss"
  (lambda ()
    (pretty-print
      '(library (B) (export x w)
         (import (rnrs) (A))
         (define w (cons (z 12) x)))))
  'replace)
(compile-imported-libraries #t)
(import (B))
(library-exports '(A)) ; ==> (x z) ; or (z x)
(library-exports '(A (1 2))) ; ==> (x z) ; or (z x)
(library-exports '(B)) ; ==> (x w) ; or (w x)
(library-version '(A)) ; ==> (1 2)
(library-version '(B)) ; ==> ()
(library-requirements '(A)) ; ==> ((rnrs (6)))
(library-requirements '(B)) ; ==> ((rnrs (6)) (A (1 2)))
(library-object-filename '(A)) ; ==> "A.so"
(library-object-filename '(B)) ; ==> "B.so"
  • 语法:(library-requirements-options symbol ...)
  • 返回:一个 library-requirements-options 枚举集
  • 所属库:(chezscheme)

library-requirements-options 枚举集被传递给 library-requirements 以确定要列出的库要求。可用选项如下所述。

import:
包括导入指定库时必须导入的库。
visit@visit:
包括访问指定库时必须访问的库。
invoke@visit:
包括访问指定库时必须调用的库。
invoke:
包括在调用指定库时必须调用的库。