Common Lisp Package 白痴指南

Common Lisp Package 白痴指南

1.介绍

当多个程序员编写大型项目时,两个不同的程序员通常可能会为两个不同的目的使用相同的名字。可以使用命名约定来解决这个问题,例如 Bob 在他所有的名字前加上“BOB-”,而 Jane 在她所有的名字前加上“JANE-”。这实际上就是 Scheme 解决这个问题的办法。(R6RS之后的 Scheme 标准也已经有了“模块”的标准了,所以这个问题已经不存在了。)

Common Lisp 提供了一个语言机制来隔离不同的命名空间。下面是包如何工作的示例:

? (make-package :bob :use '(common-lisp))
#<Package "BOB">

? (make-package :jane :use '(common-lisp))
#<Package "JANE">

? (in-package :bob)
#<Package "BOB">

? (defun foo () "This is Bob's foo")
FOO

? (in-package :jane)
#<Package "JANE">

? (defun foo () "This is Jane's foo")
FOO

? (foo)
"This is Jane's foo"

? (in-package :bob)
#<Package "BOB">

? (foo)
"This is Bob's foo"

(注意:代码示例是从 Macintosh Common Lisp (MCL) 中剪切和粘贴的。MCL 的命令提示符是一个问号。)

(注意make-package中的参数:use '(common-lisp)是译者加上去的,sbcl 在定义新的包时不会自动:use :common-lisp这个包,导致进入新的包以后,连defun都无法执行,也无法使用in-package切换到别的包。所以,原来的代码在sbcl上是无法运行的。考虑到目前sbcl基本上是使用人数最多的实现,所以译者作了一点修改,使得代码可以在sbcl上跑起来.)

如果 Bob 想要使用 Jane 编写的函数怎么办呢?有几种方法可以做到。一种是使用特殊语法来指定要使用不同的包:

? (in-package :bob)
#<Package "BOB">

? (jane::foo)
"This is Jane's foo"

另一个办法是将他想要使用的东西导入到自己的包中。当然,Bob 可能不会想要导入 Jane 的 foo 函数,因为会和他自己的 foo 函数发生冲突。如果 Bob 想要导入 Jane 的 baz函数,他希望简单地通过 (baz) 去调用该函数,而不是输入(jane::baz),他可以像这样做:

? (in-package :jane)
#<Package "JANE">

? (defun baz () "This is Jane's baz")
BAZ

? (in-package :bob)
#<Package "BOB">

? (import 'jane::baz)
T

? (baz)
"This is Jane's baz"

可惜,事情并不总是那么顺利:

? (in-package :jane)
#<Package "JANE">

? (defun bar () "This is Jane's bar")
BAR

? (in-package :bob)
#<Package "BOB">

? (bar)
> Error: Undefined function BAR called with arguments () .

;; 哦,忘了 import

? (import 'jane::bar)
> Error: Importing JANE::BAR to #<Package "BOB"> would conflict with
symbol BAR .

;; 咦, 又出错了?怎么回事?

要了解为什么会发生这种情况,如何处理,以及包的许多其他微妙之处以及惊喜,重要的是要了解包的实际作用以及它们是如何工作的。例如,重要的是要理解,当您键入 (import ‘jane::foo) 时,您导入的是符号JANE::FOO,而不是与该符号关联的函数。理解这个差异很重要,因此我们必须从回顾一些基本的 Lisp 概念开始。

2.符号,值,以及 REPL

Lisp 在 READ-EVAL-PRINT 循环中运行。大多数有趣的事情发生在 EVAL 阶段,但是当谈到包时,有趣的事情发生在所有三个阶段,重要的是要了解什么时候发生。特别是,一些与包相关的处理会在 READ 时改变 Lisp 系统的状态,这反过来又会导致一些令人惊讶(常常是令人讨厌的)的行为,就像上一节中的最后一个例子。

包是 Lisp 符号的集合,因此要了解包,您首先必须了解符号。符号是一个非常普通的 Lisp 数据结构,就像列表、数字、字符串等一样。有用于创建和操作符号的内置 Lisp 函数。例如,有一个名为 gentemp 的函数可以创建新符号:

? (gentemp)
T1

? (setq x (gentemp))
T2

? (set x 123) ;; 注意,这里使用的是 set, 不是 setq 或 setf
123

? x
T2

? t2
123

(如果您不熟悉 SET 函数,现在就是查文档的好时机,因为如果您不搞清楚,您很快就会迷失方向。)

GENTEMP 创建的符号在所有方面的行为与你通过直接输入而产生的符号相当类似。

您只能有限地控制由 GENTEMP 创建的符号的名字。您可以向它传递一个可选的前缀字符串,但系统会自动 添加一个后缀,您必须接受它给您的任何内容。如果要使用特定的名字创建符号,则必须使用另外的函数 MAKE-SYMBOL:

(make-symbol "MY-SYMBOL")
#:MY-SYMBOL

嗯,这很奇怪。那个看起来很有趣的前缀#:在做什么?

要理解这一点,我们必须更深入地挖掘符号的内涵。

? (setq symbol1 (make-symbol "MY-SYMBOL"))
#:MY-SYMBOL

? (setq symbol2 (make-symbol "MY-SYMBOL"))
#:MY-SYMBOL

? (setq symbol3 'my-symbol)
MY-SYMBOL

? (setq symbol4 'my-symbol)
MY-SYMBOL

? (eq symbol1 symbol2)
NIL

? (eq symbol3 symbol4)
T

如您所见,MAKE-SYMBOL 可以生成多个具有相同名字的不同的符号,而直接输入相同的('my-symbol)两次而得到的符号是相同的符号。

符号的这种特性非常重要。它确保了您在一个地方输入的FOO与其它地方输入的FOO是相同的符号,否则,就会得到一些非常奇怪的结果:

? (set symbol1 123)
123

? (set symbol2 456)
456

? (setq code-fragment-1 (list 'print symbol1))
(PRINT #:MY-SYMBOL)

? (setq code-fragment-2 (list 'print symbol2))
(PRINT #:MY-SYMBOL)

? (eval code-fragment-1)
123
123

? (eval code-fragment-2)
456
456

对比一下:

? (set symbol3 123)
123

? (set symbol4 456)
456

? (setq code-fragment-3 (list 'print symbol3))
(PRINT MY-SYMBOL)

? (setq code-fragment-4 (list 'print symbol4))
(PRINT MY-SYMBOL)

? (eval code-fragment-3)
456
456

? (eval code-fragment-4)
456
456

symbol1-symbol4 都具有名字“MY-SYMBOL”,但符号 1 和 2 是不同的符号,而符号 3 和 4 是相同的符号。这怎么发生的?嗯,一个明显的区别是我们调用函数 MAKE-SYMBOL 来生成符号 1 和 2,而符号 3 和 4 是由 Lisp reader(读取器)为我们生成的。也许 Lisp 读取器生成符号的方式与调用 MAKE-SYMBOL 生成符号并不相同.我们可以检验这个假设:

? (trace make-symbol)
NIL

? 'foobaz
 Calling (MAKE-SYMBOL "FOOBAZ")
 MAKE-SYMBOL returned #:FOOBAZ
FOOBAZ

[注:reader 读取到一个新符号时会调用 make-symbol 来产生新符号这种行为仅仅在 MCL & Clozure CL 测试中发生,其它实现可能采取不一样的实现方式。]

显然 reader 通过在内部调用 MAKE-SYMBOL 以与我们直接调用MAKE-SYMBOL相同的方式产生符号。但是等等,MAKE-SYMBOL 返回的符号前面有那个有趣的#: 前缀,但是最后,那个前缀消失了。这是如何发生的?

我们可以在对MAKE-SYMBOL保持跟踪的情况下再次尝试相同的实验来找出答案:

? 'foobaz
FOOBAZ

啊哈!我们第二次输入 'FOOBAZ 时,reader 不再调用 MAKE-SYMBOL。因此,reader显然保留了它所创建的所有符号的集合,并且在创建新符号之前,它会检查该集合以查看是否已经存在同名的符号。如果有,那么它只是返回相同的符号而不是创建一个新的。作为此类集合成员的符号会丢失其神秘的 #: 前缀。

这个符号的集合就是包(package)。

包是 Lisp 符号的集合,其特性是集合中不可能有两个名字相同的符号。

不幸的是,这或多或少是包简单明了的最后一个方面。从这里开始,事情变得更加棘手。

3.Interning

将符号放入包中的行为称为 interning (内部化)符号。作为包成员的符号被称为 interned (被内置)在该包中。不属于任何包成员的符号被称为 uninterned (未内部化)。当一个没有宿主包的符号被打印出来时,它就被附上前缀 #:,以便于将它与内部的符号相区分。并且提示,它与你之前见过的符号仅仅是相似,它们实际上可能不是同一个符号。

[译注] intern 实际上是 internal 的简写

现在,事情开始变得有点混乱。

有一个名为INTERN的 Lisp 函数,凭直觉您可能会认为它能够将不属于任何包的符号添加到某个包中,但事实并非如此。这个功能由名为import的函数提供的:

? symbol1
#:MY-SYMBOL

? (import symbol1)
T

? symbol1
MY-SYMBOL

? (eq symbol1 'my-symbol)
T

如您所见,symbol1 已经从一个未内部化的符号变成了一个内部化的符号。它已经失去了#: 前缀,现在它等同于由 Lisp reader产生的符号 MY-SYMBOL

现在,你可能想调用UNIMPORT来撤销掉IMPORT的效果,但是并没有这样的函数。(我警告过你,事情不会那么简单),要从包中删除一个符号,你可以调用UNINTERN:

? (unintern symbol1)
T

? symbol1
#:MY-SYMBOL

? (eq symbol1 'my-symbol)
NIL

现在,事情又回到原来的样子。symbol1又变成未内部化的状态,它和 reader 返回给你的符号也不再相同。让我们再一次把它 import 到包里来:

? (import symbol1)
> Error: Importing #:MY-SYMBOL to #<Package "COMMON-LISP-USER"> would
conflict with symbol MY-SYMBOL .

又发生了什么?(在进一步阅读之前尝试弄清楚这里发生了什么是一个很好的练习。你已经有足够的信息来弄清楚事实。提示:你可以尝试使用 trace make-symbol 的实验)

这就是所发生的事情:

? (unintern 'my-symbol)
T
? (eq symbol1 'my-symbol
 Calling (MAKE-SYMBOL "MY-SYMBOL")
 MAKE-SYMBOL returned #:MY-SYMBOL
)
NIL

当我们执行测试 (eq symbol1 'my-symbol)的时候,reader 已经悄悄地调用MAKE-SYMBOL产生并且内部化了一个符号,当我们再次 import 的时候,包里已经有一个同名的符号了。这种情况称为符号冲突,唉,这种情况相当普遍。

请注意,顺便说一下,上面 MAKE-SYMBOL 跟踪的输出出现在 S 表达式(eq symbol1 ‘my-symbol)中。这是因为 MCL 重新排列了屏幕上的文本以反映事件的真实顺序。因为 MAKE-SYMBOL 在处理字符串“my-symbol”时被读取器调用,但在处理关闭括号之前,输出出现在那个点。如果我们输入: (list 'x1 'x2 'x3 'x4) 结果输出如下所示:

? (list 'x1
 Calling (MAKE-SYMBOL "X1")
 MAKE-SYMBOL returned #:X1
'x2
 Calling (MAKE-SYMBOL "X2")
 MAKE-SYMBOL returned #:X2
'x3
 Calling (MAKE-SYMBOL "X3")
 MAKE-SYMBOL returned #:X3
'x4
 Calling (MAKE-SYMBOL "X4")
 MAKE-SYMBOL returned #:X4
)

在这一点上,我们可以继续并提及一些更有用的函数。 SYMBOL-NAME 以字符串形式返回符号的名字。 FIND-SYMBOL 接受一个字符串并告诉您是否已经存在具有该名字的符号。最后是 INTERN,其实它可以像这样定义:

(defun intern (name)
  (or (find-symbol name)
      (let ((s (make-symbol name)))
        (import s)
        s)))

换句话说,INTERNSYMBOL-NAME 相反。 SYMBOL-NAME 接受一个符号并返回该符号名字的字符串。 INTERN 接受字符串并返回名字为该字符串的符号。INTERNREAD 用来制作符号的函数。

intern string &optional package => symbol, status

参数和返回值:

string - 一个字符串。

package - 包描述符,默认为当前包。

symbol - 一个符号.

status - 下列之一 :inherited, :external, :internal, or nil.

可以把 intern理解为UNIX命令 touch,当文件不存在时,touch只修改文件的时间戳,当文件不存在时,它会自创建一个新文件。而intern在指定的符号不存在时会自动创建该符号,当符号存在时,则什么也不做,只是返回该符号。

4.哪一个包

对包进行操作的 FIND-SYMBOLIMPORTINTERN 等函数默认的操作对象是全局变量*PACKAGE* 的值所标识的包,也称为当前包。和符号一样,包也有名字,系统中不可能同时存在两个相同名字的包。包和符号一样,也是普通的 Lisp 数据结构。有一些函数 PACKAGE-NAMEFIND-PACKAGE 的操作类似于 SYMBOL-NAMEFIND-SYMBOL,当然,除了FIND-PACKAGE不接受 PACKAGE 参数。 (就好像有一个全局的元包一样。)

正如我们已经看到的,我们可以通过调用 MAKE-PACKAGE 来创建新包,并通过调用 IN-PACKAGE 设置当前包。 (请注意, IN-PACKAGE 不是函数,而是不其参数求值的宏。)

您可以使用特殊语法访问不在当前包中的符号:包名后跟两个冒号,后跟符号名。如果您只想使用符号而不导入它,这样做很方便。例如,如果 Bob 想使用 Jane 的 FOO 函数,他可以输入 JANE::FOO

4.1 home 包

Common Lisp 试图维护的不变量之一是称为"打印-读取一致性"的属性。此属性表示,如果您打印一个符号,稍后再读取该符号的打印表示,结果应该得到相同的符号,但有两个警告:1)这不适用于非内部化的符号,2)仅当您避免某些“危险”的行为的时候。我们稍后会介绍有哪些操作会影响到“打印-读取一致性”。

为了保持打印读取的一致性,一些符号需要使用它们的包限定符来打印。例如:

? (in-package jane)
#<Package "JANE">

? 'foo
FOO

? 'jane::foo
FOO

? (in-package :bob)
#<Package "BOB">

? 'foo
FOO

? 'jane::foo
JANE::FOO

? 'bob::foo
FOO

显然,会影响到"打印读取一致性"的“危险操作”之一是调用 IN-PACKAGE

现在,思考以下情况:

? (in-package 'bob)
#<Package "BOB">

? (unintern 'foo)
T

? (import 'jane::foo)
T

? (make-package :charlie)
#<Package "CHARLIE">

? (in-package :charlie)
#<Package "CHARLIE">

? 'bob::foo
JANE::FOO

这里我们有一个符号 FOO,它同时存在于 JANEBOB 包中。因此,该符号可以同时作为 JANE::FOOBOB::FOO 访问。当从 CHARLIE 包中打印符号时,系统如何决定使用哪种打印表示来打印非内置的符号?

事实证明,每个符号都跟踪一个称为其 home package 的包,它通常是该符号所在的第一个包(但也有例外)。当一个符号需要用包限定符打印时,它使用来自它的 home 包的限定符。您可以使用函数 SYMBOL-PACKAGE 查询其 home 包的符号。

? (symbol-package 'bob::foo)
#<Package "JANE">
?

请注意,可以在没有 home 的情况下制作符号。例如,未内部化的包。还可以在没有 home 包的情况下制作已内部化的包,比如:

? (in-package :jane)
#<Package "JANE">

? 'weird-symbol
WEIRD-SYMBOL

? (in-package :bob)
#<Package "BOB">

? (import 'jane::weird-symbol)
T

? (in-package :jane)
#<Package "JANE">

? (unintern 'weird-symbol)
T

? (in-package 'bob)
#<Package "BOB">

? 'weird-symbol
WEIRD-SYMBOL

? (symbol-package 'weird-symbol)
NIL

? (in-package :jane)
#<Package "JANE">

? 'bob::weird-symbol
#:WEIRD-SYMBOL

这其实是需要避免发生的事情。

5. export 和 use

假设 Jane 和 Bob 正在合作进行一个软件开发项目,每个人都在自己的包中工作以避免冲突。简写道:

? (in-package 'jane)
#<Package "JANE">

? (defclass jane-class () (slot1 slot2 slot3))
#<STANDARD-CLASS JANE-CLASS>

现在,假设 Bob 想要使用 JANE-CLASS,他写道:

? (in-package :bob)
#<Package "BOB">

? (import 'jane::jane-class)
T

? (make-instance 'jane-class)
#<JANE-CLASS #x130565E>

到目前为止没什么问题。现在他尝试:

? (setq jc1 (make-instance 'jane-class))
#<JANE-CLASS #x130BA96>

? (slot-value jc1 'slot1)
> Error: #<JANE-CLASS #x130BA96> has no slot named SLOT1.

发生了什么?好吧,JANE-CLASS是在 JANE包中定义的,所以,槽的名字是该包中的符号,但是 Bob 尝试使用 BOB 包中的符号访问该类的实例。换句话说,JANE-CLASS 有一个名为 JANE::SLOT1的槽,而 Bob 尝试访问一个名为 BOB::SLOT1的槽,然而并不存在这样的槽。

Bob 真正想做的是导入与 JANE-CLASS 有“关联”的所有符号,即所有的槽名、方法名等。他怎么知道所有这些符号是什么?他可以检查 Jane 的代码并尝试自己弄清楚,但这会导致许多问题,尤其是他可能决定导入一个 Jane 不想让 Bob 乱搞的符号(记住,将 Jane 的符号与 Bob 的瞎搞可能造成的影响隔离开就是包存在的全部意义)。

更好的解决方案是让 Jane 收集起一个 Bob 应该导入的符号列表,以方便 Bob 使用她的软件。然后 Jane 可以执行以下操作:

? (defvar *published-symbols* '(jane-class slot1 slot2 slot3))
*PUBLISHED-SYMBOLS*

然后 Bob 可以 (import jane::*published-symbols*).

Common Lisp 提供了一个标准的机制来做到这一点。每个包都维护着一个旨在供其他包使用的符号列表。此列表称为该包的导出符号列表,要向该包添加符号,请使用 EXPORT 函数。要从包的已导出符号列表中删除符号,请使用(正如您的直觉所期望的那样)UNEXPORT。要导入包中的所有导出符号,请调用 USE-PACKAGE。 反导入它们你可以使用UNUSE-PACKAGE

关于导出符号有两点需要注意。

首先,一个符号可以从内部化了该符号的任意包中导出,而不仅仅是它的主包。一个符号也不必非要从它的主包导出,就可以从该符号所在的其他包中导出。[注:绕来绕去,说的是:p2 导入了 p1 中的符号,甭管是 use 还是 import 导入的,p2 都可以将它 export, 然后,p3 可以 use p2, 间接地导入 p1 中的符号]

其次,从其主包导出的符号在使用其包限定符打印时仅使用一个冒号而不是两个。这是为了提醒您符号是从其主包导出的事实(因此您可能希望use-package导入符号所属的包而不是用import导入符号本身),并通过强制您键入额外的冒号来阻止您使用未导出的符号以便访问它们。 (不,我没有开玩笑。)

6.阴影

最后还有一个问题:使用 USE-PACKAGEIMPORT包中的所有导出符号略有不同。当您IMPORT一个符号时,您可以使用 UNINTERN 取消导入的效果。您不能使用 UNINTERN 来(部分)撤销 USE-PACKAGE 的效果。例如:

? (in-package :jane)
#<Package "JANE">

? (export '(slot1 slot2 slot3))
T

? (in-package :bob)
#<Package "BOB">

? (use-package 'jane)
T

? (symbol-package 'slot1)
#<Package "JANE">

? (unintern 'slot1)
NIL

? (symbol-package 'slot1)
#<Package "JANE">

这是一个问题,它似乎让使用两个不同的包导出具有相同的符号变得不可能。例如,假设您想在名为 MYPACKAGE 的第三个包中USE了两个包 P1P2,并且 P1P2 都导出了一个名为 X 的符号。如果你尝试同时 use-package p1p2,你就会遇到名字冲突,因为 Lisp 搞不清楚 MYPACKAGE 中的 X 现在应该是 P1:X 还是 P2:X

为了解决这种名字冲突,每个包都维护了一个所谓的 影子符号列表 。影子符号列表是一个符号列表,它会隐藏或覆盖掉由 USE-PACKAGE 导入的外部符号。

有两种方法可以将符号添加到包的影子符号列表中,SHADOWSHADOWING-IMPORTSHADOW 用于将包中的符号添加到影子符号列表中。 SHADOWING-IMPORT 用于添加其他包中的符号。

[说人话: shadow 屏蔽掉从外部引入的同名符号,shadowing-import 则屏蔽掉与指定的外部符号冲突的其它符号]

例如:

? (in-package :p1)
#<Package "P1">

? (export '(x y z))
T

? (in-package :p2)
#<Package "P2">

? (export '(x y z))
T

? (in-package :bob)
T

? (use-package :p1)
T

? (use-package :p2)
> Error: Using #<Package "P2"> in #<Package "BOB">
>        would cause name conflicts with symbols inherited
>        by that package:
>        Z  P2:Z
>        Y  P2:Y
>        X  P2:x
...
1 > 
Aborted
? (unuse-package :p1)
T

? (shadow 'x)
T

? (shadowing-import 'p1:y)
T

? (shadowing-import 'p2:z)
T

? (use-package :p1)
T

? (use-package :p2)
T

? (symbol-package 'x)
#<Package "BOB">

? (symbol-package 'y)
#<Package "P1">

? (symbol-package 'z)
#<Package "P2">

要撤销 SHADOWSHADOWING-IMPORT 的效果,请使用 UNINTERN

请注意,UNINTERN(以及许多其他令人惊讶的事情)可能会导致意外地出现名字冲突。意外的名字冲突的最常见原因通常是通过在 reader 中键入它而无意中将符号插入包中,而没有密切注意您当时处在哪个包中。

7.DEFPACKAGE

现在您已经了解了无数用于操作包的函数和宏,但是您不应该真正直接使用它们中的任何一个。相反,IMPORTEXPORTSHADOW 等的所有功能都集中在一个名为 DEFPACKAGE 的宏中,您应该将其用于实际(非原型)代码。

我不打算在这里解释 DEFPACKAGE,因为现在您了解了包的基本概念,您应该能够阅读 hyperspec 中的文档并理解它。 (在 hyperspec 中还有很多其他好东西,例如 DO-SYMBOLSWITH-PACKAGE-ITERATOR,您现在应该能够自己理解了。)

不过,关于使用 DEFPACKAGE 的一个警告:请注意,DEFPACKAGE 的大多数参数是字符串,而不是符号。这意味着虽然它们可以是符号,但是如果您选择使用符号,那么在读取 DEFPACKAGE 表单时,无论它是什么,这些符号都将被内部化在当前包中。这通常会导致不良后果,因此养成在 DEFPACKAGE 表单中使用关键字参数或字符串的习惯是个好主意。

8.最后的想法

了解包最重要的一点是,它们从根本上是 Lisp 读取器的一部分,而不是求值器的一部分。一旦你的大脑围绕这个想法,其他一切都会水到渠成。包控制读取器如何将字符串映射到符号(以及 PRINT 如何将符号映射到字符串),没有别的。特别是,包与可能与或可能不与任何特定符号相关联的函数、值、属性列表等无关。

特别要注意(这与包无关,但无论如何新手经常对此感到困惑)符号和函数对象都可以用作函数,但它们的行为略有不同。例如:

? (defun foo () "Original foo")
FOO

? (setf f1 'foo)
FOO

? (setf f2 #'foo)
#<Compiled-function FOO #xEB3446>

? (defun demo ()
    (list (funcall f1) (funcall f2)
          (funcall #'foo) (funcall #.#'foo)))
;Compiler warnings :
;   Undeclared free variable F1, in DEMO.
;   Undeclared free variable F2, in DEMO.
DEMO

? (demo)
("Original foo" "Original foo" "Original foo" "Original foo")

? (defun foo () "New foo")
FOObaz
? (demo)
("New foo" "Original foo" "New foo" "Original foo")

在这个例子中,我们有两个变量,F1F2F1 的值是符号FOOF2 的值是在分配 F2 时位于符号 FOO 的符号函数槽中的函数对象。

请注意,当重新定义 FOO 时,调用符号 FOO 的效果是获得新行为,而调用函数对象会产生旧行为。

解释列表中的第二个和第三个结果留给读者练习(没有双关语)。

posted @ 2022-01-06 15:05  fmcdr  阅读(222)  评论(0编辑  收藏  举报