附录: 包(packages)

2018-02-24 15:54 更新

附录: 包(packages)

包(packages),是 Common Lisp 把代码组织成模块的方式。早期的 Lisp 方言有一张符号表,即oblist【注1】。在这张表里列出了系统中所有已经读取到的符号。借助 oblist 里的符号表项,系统得以存取数据,诸如对象的值,以及属性列表等。保存在 oblist 里的符号被称为 interned。

新一些的 Lisp 方言把 oblist 的概念放到了一个个包里面。现在,符号不仅仅是被 intern 了,而是被 intern 在某个包里。包之所以支持模块化是因为在一个包里的 intern 的符号只有在其被显式声明为能被其它包访问的时候,它才能为外部访问(除非用一些歪门邪道的招数)。

包是一种 Lisp 对象。当前包常常被保存在一个名为 \*package\* 的全局变量里面。当 Common Lisp 启动时,当前包就是用户包:或者叫 user (CLTL1 实现),或者叫 common-lisp-user (CLTL2实现)。

包一般用自己的名字相互区别,而这些名字采用的是字符串的形式。要知道当前包的包名,可以试试:

> (package-name *package*)
"COMMON-LISP-USER"

通常,当读入一个符号时,它就被 intern 到当前的包里了。要弄清给定符号所 intern 的是哪个包,我们可以

用 symbol-package :

> (symbol-package 'foo)
#<Package "COMMON-LISP-USER" 4CD15E>

这个返回值是实际的包对象。为便于将来使用,我们给 foo 赋一个值:

> (setq foo 99)
99

使用 in-package ,我们就可以切换到另一个新的包,若有需要的话这个包会被创建出来【注2】:

> (in-package 'mine :use 'common-lisp)
#<Package "MINE" 63390E>

此时此刻应该会响起诡异的背景音乐,因为我们已经身处另一个世界:在这里 foo 已经不似从前了:

MINE> foo
>>Error: FOO has no global value.

为什么会这样?因为之前被我们设置成 99 的那个 foo 和现在 mine 里面的这个 foo 是两码事。【注3】要从用户包之外引用原来的这个 foo ,我们必须把包名和两个冒号作为它的前缀:

MINE> common-lisp-user::foo
99

因此,具有相同打印名称的不同符号得以在不同包中共存。这样就可以在名为 common-lisp-user 的包里有一个 foo ,同时在 mine 包里也有一个 foo ,并且它们两个是不一样的符号。实际上,这就是 package 的一部分用意所在,即:你在为你的函数和变量取名字的同时,就不用担心别人会把一样的名字用在其它东西上。现在,就算有重名的情况,重名的符号之间也是互不相干的。

与此同时,包也提供了一种信息隐藏的手段。对程序来说,它必须使用名字来引用不同的函数和变量。如果你不让一个名字在你的包之外可见的话,那么另一个包中的代码就无法使用或者修改这个名字所引用的对象。

在写程序的时候,把包的名字带上两个冒号做为前缀并不是个好习惯。你要是这样做的话,就违背了模块化设计的初衷,而这正是包机制的本意。如果你不得不使用双冒号来引用一个符号,这应该就是有人根本就不希望你引用它。

一般来说,你只应该引用那些被 export 了的符号。把符号从它所属的包 export 出来,我们就能让这个符号对其它包变得可见。要导出一个符号,我们可以调用(你肯定已经猜到了) export :

MINE> (in-package 'common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (export 'bar)
T
> (setq bar 5)
5

现在,如果回到了 mine 包,那么就可以用一个冒号引用 bar ,因为这个名字是外部可见的:

> (in-package 'mine)
#<Package "MINE" 63390E>
MINE> common-lisp-user:bar
5

如果把 bar import 到 mine 里面,我们就能更进一步,让 mine 能和 user 包共享 bar 这个符号:

MINE> (import 'common-lisp-user:bar)
T
MINE> bar
5

在导入 bar 之后,我们可以根本不用加任何包的限定符,就能引用它了。现在,这两个包共享了同一个符号 -- 再没有一个独立的 mine:bar 了。

万一已经有了一个会怎么样呢?在这种情况下,import 调用会导致一个错误,就像下面我们试着import foo 时造成的错误一样:

MINE> (import 'common-lisp-user::foo)
>>Error: FOO is already present in MINE.

之前,我们在 mine 里对 foo 进行了一次不成功的求值,这次求值顺带着使得一个名为 foo 的符号被加入了 mine 。由于这个符号在全局范围内还没有值,因此产生了一个错误,但是输入符号名字的直接后果就是使它被 intern 进了这个包。所以,当我们现在想把 foo 引进 mine 的时候,mine里面已经有一个相同名字的符号了。

通过让一个包使用 (use) 另一个包,我们也能批量的引入符号:

MINE> (use-package 'common-lisp-user)
T

这样,所有 user package 引出的符号就会自动地被引进到 mine 里面去了。(要是 user``package 已经引出了 foo 的话,这个函数调用也会出一个错。)

根据 CLTL2,包含内建操作符和变量名字的包被称为 common-lisp 而不是 lisp ,因此新一些的包在缺省情况下已不再使用 lisp 包了。由于我们通过调用in-package 创建了 mine ,而在这次调用中也 use 了这个包,所以所有 Common Lisp 的名字在 mine 中都是可见的:

MINE> #'cons
#<Compiled-Function CONS 462A3E>

在实际的编程中,你不得不让所有新编写的包使用 common-lisp (或者其他某个含 Lisp 操作符的包)。否则你甚至会没办法跳出这个新的包。【注4】

一般来说,在编译后的代码中,不会像刚才这样在顶层进行包的操作。更多的时候,这些关于包的函数调用会被包含在源文件中。通常,只要把 in-package 和 defpackage 放在源文件的开头就可以了。

(defpackage 宏是 CLTL2 里新引进的,但是有些较老的实现也提供了它。) 如果你要编写一个独立的包,下面列出了你可能会放在对应的源文件最开始地方的代码:

(in-package 'my-application :use 'common-lisp)
(defpackage my-application
  (:use common-lisp my-utilities)
  (:nicknames app)
  (:export win lose draw))

这会使得该文件里所有的代码,或者更准确地说,文件里所有的名字,都纳入了 my-application 这个包。

my-application 同时使用了 common-lisp 和 my-utilities ,因此,不用加任何包名作为前缀,所有被引出的符号都可以直接使用。

my-application 本身仅仅引出了三个符号,它们分别是:winlose 和 draw 。由于在调用in-package 的时候,我们给 my-application 取了一个绰号 app ,在其它包里面的代码可以用类似 app:win 的名字来引用这些符号。

像这样的用包来提供的模块化的确有点不自然。我们的包里面不是对象,而是一堆名字。每个使用common-lisp 的包都引入了 cons 这个名字,原因在于 common-lisp 包含了一个叫这个名字的函数。但是,这样会导致一个名字叫 cons 的变量也在每个使用 common-lisp 的程序里可见。这样的事情同样也会在 Common Lisp 的其他名字空间重演。如果包(package) 这个机制让你头痛,那么这就是一个最主要的原因 -- 包不是基于对象而是基于名字。

和包相关的操作会发生在读取时(read-time),而非运行时。这可能会造成一些困扰。我们输入的第二个表达式:

(symbol-package 'foo)

之所以会返回它返回的那个值是因为:读取这个查询语句的同时,答案就被生成了。为了求值这个表达式,Lisp 必须先读入它,这意味着要 intern foo

再来个例子,看看下面把两个表达式交换顺序的结果,这两个表达式前面曾出现过:

MINE> (in-package 'common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (export 'bar)

通常来说,在顶层输入两个表达式的效果等价于把这两个表达式放在一个progn 里面。不过这次有些不同。如果我们这样说

MINE> (progn (in-package 'common-lisp-user)
  (export 'bar))
>>Error: MINE::BAR is not accessible in COMMON-LISP-USER.

则会得到个错误提示。错误的原因在于 progn 表达式在求值之前就已经被 read 处理过了。当调用 read 时,当前包还是 mine ,因而 bar 被认为是 mine:bar 。运行这个表达式的效果就好像我们想要从 user 包 export 出 mine:bar ,而不是从 common-lisp-user export 出 common-lisp-user:bar 一样。

package 被如此定义,使得编写那些把符号当作数据的程序成为一桩麻烦事。举个例子,要是像下面那样定义 noise :

(in-package 'other :use 'common-lisp)
(defpackage other
  (:use common-lisp)
  (:export noise))

(defun noise (animal)
  (case animal
    (dog 'woof)
    (cat 'meow)
    (pig 'oink)))

这样的话,如果我们从另外一个包调用 noise ,同时传进去的参数是不认识的符号,noise 会走到 case 语句的末尾,并返回 nil :

OTHER> (in-package 'common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (other:noise 'pig)
NIL

这是因为传进去的参数是 common-lisp-user:pig (这没有冒犯阁下的意思),然而 case 接受 key是 other:pig 。为了让 noise 像我们期望的那样工作,就必须把里面用到的所有六个符号都引出来,再在调用 noise 的包里面引入它们。

在此例中,我们也可以通过使用关键字而不是常规的符号,来绕过这个问题。倘若 noise 像下面这样定义:

(defun noise (animal)
  (case animal
    (:dog :woof)
    (:cat :meow)
    (:pig :oink)))

的话,我们就能从任意一个包安全地调用这个函数了:

OTHER> (in-package 'common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (other:noise :pig)
:OINK

关键字就像金子:普适而且自身就能表明其价值。不论在哪里它们都是可见的,而且它们从不需要被引用。

在编写类似 defanaph ( 16.3 节) 的符号驱动的函数时,基本上应该总是用关键字参数。

包里面有很多地方让人不解。这里对这一主题的介绍不过是冰山一角。要知道所有的细节,请参考CLTL2 的第 11 章。

备注:

【注1】译者注:GNU Emacs 和 XEmacs 使用的是一张名为 obarray 的哈希表。

【注2】在较早期的 Common Lisp 实现下,请省略掉 :use 参数

【注3】有的 Common Lisp 实现会在 toplevel 提示符的前面显示包的名字。这个特性不是必须的,但的确是比较贴心的设计。

【注4】译者注:即你不仅没有办法使用cons ,更糟糕的是,你也不能用in-package 切换到其它包。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号