Common Lisp包系统完全指南

原创
2015/04/02 09:37
阅读数 1.8K

原文请在此处下载:http://download.csdn.net/detail/firstblood2008/8550683

1. 简介

当与多名程序员编写大型工程的时候,两个不同的程序员经常会因不同的意图而使用相同的名称。使用命名空间惯例可能会解决这个问题,例如,Bob在他所有的变量名前都加上"BOB-"前缀,Jane在她所有的变量名前加上"JANE-"前缀。事实上,这就是Scheme对付这个问题的办法(视情况也可以说是其应付失败的地方)。

Common Lisp提供了一种叫做包的语言机制来分离命名空间,这里有一个包如何工作的例子:

CL-USER> (make-package :bob :use '(common-lisp cl-user))
#<PACKAGE "BOB">
CL-USER> (make-package :jane :use '(common-lisp cl-user))
#<PACKAGE "JANE">

CL-USER> (in-package bob)
#<PACKAGE "BOB">
BOB> (defun foo () "This is Bob's foo")
FOO
BOB> (in-package JANE)
#<PACKAGE "JANE">

JANE> (defun foo () "This is jane's foo")
FOO
JANE> (foo)
"This is jane's foo"
JANE> (in-package :bob)
#<PACKAGE "BOB">
BOB> (foo)
"This is Bob's foo"
BOB>
Bob和Jane没人都有一个名为FOO的函数,它们的行不有些不同,彼此之间不会冲突。

要是Bob想要使用Jane编写的foo该怎么办呢?这里有几种方法来做到。一种就是使用特殊的语法来表示不同的包将会被使用:

BOB> (in-package bob)
#<PACKAGE "BOB">
BOB> (jane::foo)
"This is jane's foo"
BOB>
另一种方法就是将你想要使用的符号导入到你的包内。当然,他不想将jane的FOO函数导入,原因是它会与自己的FOO函数相冲突。但是,如果Jane有一个BAR函数,他想要通过键入(BAZ )的方式来代替(JANE::BAR),他可以这样做:

BOB> (in-package jane)
#<PACKAGE "JANE">
JANE> (defun baz () "This is Jane's foo")
BAZ
JANE> (in-package bob)
#<PACKAGE "BOB">
BOB> (import 'jane::baz)
T
BOB> (baz)
"This is Jane's foo"
哎呀,事情经常不会太顺利:

BOB> (in-package jane)
#<PACKAGE "JANE">
JANE> (defun bar () "This is Jane's bar")
BAR
JANE> (in-package bob)
#<PACKAGE "BOB">
BOB> (bar) ;;;注:此处是找不到符号

; in: BAR
;     (BOB::BAR)
; 
; caught STYLE-WARNING:
;   undefined function: BAR
; 
; compilation unit finished
;   Undefined function:
;     BAR
;   caught 1 STYLE-WARNING condition
; Evaluation aborted on #<UNDEFINED-FUNCTION BAR {24FEC1E1}>.
BOB> (import 'jane::bar) ;;;注:此处是命名冲突
; Evaluation aborted on #<NAME-CONFLICT {251DE141}>.
BOB>
为了理解为什么会发生这个问题,对这个问题该做什么,还有包的其它的微妙与惊喜的表现,理解包实际上是干什么的和如何工作是很重要的。例如,理解以下事项是很重要的:即当你键入(import 'jane::foo)时,你导入的是符号JANE::FOO,而不是与该符号相关联的函数,理解这两点的不同是很重要的,所以我们不得不对一些基本lisp概念进行复习。

2. 符号、值和REPL

Lisp在REPL(读-求值-打印 循环)里操作。大多数有意思的事情都发生在求值阶段,但是一遇到包这些有意思的事儿在这三个阶段就都会发生了,理解什么时间发生了什么是重要的。尤其是,一些与包相关的处理过程可以在“读取”期改变Lisp系统的状态,这可能依次引出一些令人惊奇的行为(有时候也是令人恼火的),就像前一章里的最后一个例子一样。

包,就是Lisp符号的集合,要理解包你首先要理解符号。符号是一个非常普通的Lisp数据结构,就像列表,数值,字符串等等一样。有一些内建的Lisp函数专门用来创建和管理符号。例如,有一个叫gentemp的函数,它可以创建新的符号:

CL-USER> (gentemp)
T1
CL-USER> (setq x (gentemp))
T2
CL-USER> (set x 123) ; 注意这里用set,不是setf或setq
123
CL-USER> x
T2
CL-USER> t2
123
CL-USER>
(如果你对SET函数不熟悉的话,而且你不想一会儿忘了,那现在正是查阅的好时候。) 注:根据CLHS,set已被遗弃,它是动态作用域的操作符,不适用于改变词法变量的值。

由GENTEMP创建的符号和由键入名字得到的符号,他们在所有方面的表现都很像。

你对由GENTEMP创建的符号名称仅有有限控制,你可以向它传递一个可选的前缀字符串,但是系统也会加一个前缀,你又不得不接受它给你的东西。如果你想使用一个特定名称创建一个符号,你不得不使用一个不同的函数——MAKE-SYMBOL:

CL-USER> (make-symbol "my-symbol")
#:|my-symbol|
CL-USER>
嗯,有点奇怪,那个长相怪怪的"#:"是什么东西?想要理解这个,我们就不得不深入地挖掘一下符号的五脏六腑:

CL-USER> (setq symbol1 (make-symbol "MY-SYMBOL"))
#:MY-SYMBOL
CL-USER> (setq symbol2 (make-symbol "MY-SYMBOL"))
#:MY-SYMBOL
CL-USER> (setq symbol3 'my-symbol)
MY-SYMBOL
CL-USER> (setq symbol4 'my-symbol)
MY-SYMBOL
CL-USER> (eq symbol1 symbol2)
NIL
CL-USER> (eq symbol3 symbol4)
T
CL-USER>
就像你看到的,MAKE-SYMBOL可以创建多个不同的符号,它们有相同的名字;然而读取器给你的那些符号是相同的,这些符号是通过在不同的时机键入的相同名字。

符号识别的这个属性是非常重要的,这个特性保证了你在一个地方键入的FOO与在其他地方键入的FOO是同一个FOO。如果不是这样的话,你将得到一些非常怪异的结果:

CL-USER> (set symbol1 123)
123
CL-USER> (set symbol2 456)
456
CL-USER> (setq code-fragment-1 (list 'print symbol1))
(PRINT #:MY-SYMBOL)
CL-USER> (setq code-fragment-2 (list 'print symbol2))
(PRINT #:MY-SYMBOL)
CL-USER> (eval code-fragment-1)

123 
123
CL-USER> (eval code-fragment-2)

456 
456
CL-USER>
与下边这个对照一下:

CL-USER> (set symbol3 123)
123
CL-USER> (set symbol4 456)
456
CL-USER> (setq code-fragment-3 (list 'print symbol3))
(PRINT MY-SYMBOL)
CL-USER> (setq code-fragment-4 (list 'print symbol4))
(PRINT MY-SYMBOL)
CL-USER> (eval code-fragment-3)

456 
456
CL-USER> (eval code-fragment-4)

456 
456
CL-USER>
符号1-4有相同的名字"MY-SYMBOL",但是符号1与符号2是不同的符号,而符号3和符号4是相同的符号,这究竟是怎么回事儿?好吧,一个明显的不同就是我们调用了MAKE-SYMboL函数来创建符号1和符号2,而符号3和4是通过Lisp读取器创建的。与调用MAKE-SYMBOL相比,可能Lisp读取器有一种创建符号的方式。我们可以测试这个猜测:

CL-USER> (trace make-symbol)
  0: (make-symbol "FOURTH")
  0: make-symbol returned #:fourth
  0: (make-symbol "FORMATTER")
  0: make-symbol returned #:formatter
  0: (make-symbol "FORMAT")
  0: make-symbol returned #:format
  0: (make-symbol "FORCE-OUTPUT")
  0: make-symbol returned #:force-output
(MAKE-SYMBOL)
CL-USER> 'foobaz
  0: (make-symbol "FOURTH")
  0: make-symbol returned #:fourth
  0: (make-symbol "FORMATTER")
  0: make-symbol returned #:formatter
  0: (make-symbol "FORMAT")
  0: make-symbol returned #:format
  0: (make-symbol "FORCE-OUTPUT")
  0: make-symbol returned #:force-output
  0: (make-symbol "FOOBAZ")
  0: make-symbol returned #:foobaz
  0: (make-symbol "FOOBAZ")
  0: make-symbol returned #:foobaz
  0: (make-symbol "FOOBAZ")
  0: make-symbol returned #:foobaz
  0: (make-symbol "FOOBAZ")
  0: make-symbol returned #:foobaz
  0: (make-symbol "FOOBAZ")
  0: make-symbol returned #:foobaz
  0: (make-symbol "FOOBAZ")
  0: make-symbol returned #:foobaz
FOOBAZ
CL-USER>

不,通过调用MAKE-SYMBOL,表面上读取器与我们创建符号的方式相同;但是,等一下,MAKE-SYMBOL返回的符号的前边里有一个搞笑的#:,但是到读取器读取完毕为止,#:已经消失了,它给了我们什么?

通过使用带追踪能力的MAKE-SYMBOL进行第二次尝试相同的试验,我们可以发现答案:

CL-USER> 'foobaz
FOOBAZ
哈哈,我们第二次键入FOOBAR,读取器就不调用MAKE-SYMBOL了,所以,读取器显然地保存了它创建的所有的符号的集合,在它创建一个新符号之前,它首先检测该集合里是否已经有一个相同名字的符号,如果已经有了,那么它就返回该符号而不是创建一个,该集合里的这样一个符号成员再丢掉那个神秘的#:前缀。

那个符号的集合就叫做包。

包就是Lisp符号的集合,这些符号有这样一个特性,即该集合里没有两个相同名字的符号。

不幸的是,多多少少那是包简单和直接的最后一个体现,从现在开始事情就变得刺激了。

3. 驻留

(关于词意来历请参看:字符串的驻留

将一个符号放入到包里的动作叫符号驻留。作为包的成员的符号可以说是驻留在那个包中的。那些不是任何包的成员的符号可以说没有被驻留。未驻留符号打印时,将在其前面加一个#:前缀,用来与已驻留符号做区分,这个前缀的作用是提醒你:因为这个符号是另一个符号,事实上不是你以前见过的相同的那个符号。

现在,这里就是开始让你有那么一点儿困惑的地方。

有一个叫INTERN的Lisp函数,你可能希望用它来向包里加入一个符号,但是它却没起作用。该函数由一个叫做IMPORT的函数来执行:

CL-USER> 'symbol1
SYMBOL1
CL-USER> symbol1
#:MY-SYMBOL
CL-USER> (import symbol1)
T
CL-USER> symbol1
MY-SYMBOL
CL-USER> (eq symbol1 'my-symbol)
T
CL-USER>
就像你看到的,symbol1已经从一个未驻留符号变成驻留符号,它丢掉了#:前缀,现在它与Lisp读取器生成的MY-SYMBOL符号是EQ等价的。

现在,你可能想通过调用UNIMPORT来撤销IMPORT的影响,但是没有这样的一个函数。(我提醒你那样的事情不可能是直接的)为了从包里移除符号,你可以调用UNINTERN:

CL-USER> (unintern symbol1)
T
CL-USER> symbol1
#:MY-SYMBOL
CL-USER> (eq symbol1 'my-symbol)
NIL
CL-USER>
事情又回到了它原本的样子,符号1现在是未驻留的,与读取器给你的符号相比它是不同的符号,让我们再把symbol1放回到包里:
CL-USER> (import symbol1)
IMPORT #1=#:MY-SYMBOL causes name-conflicts in
#<PACKAGE "COMMON-LISP-USER"> between the following symbols:
  #1#, COMMON-LISP-USER::MY-SYMBOL
   [Condition of type NAME-CONFLICT]
See also:
  Common Lisp Hyperspec, 11.1.1.2.5 [:section]

Restarts:
 0: [RESOLVE-CONFLICT] Resolve conflict.
 1: [RETRY] Retry SLIME REPL evaluation request.
 2: [*ABORT] Return to SLIME's top level.
 3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {25547F21}>)
...........................
MLGB,怎么回事儿?(在读取任何东西之前试图解决这里发生了什么是个相当好的实践方式,这里有你需要的所有信息。暗示:你自己用MAKE-SYMBOL回溯做下试验。)

这里就是所发生的事情:

CL-USER> (unintern 'my-symbol)
T
CL-USER> (eq symbol1 'my-symbol)
NIL
CL-USER>
当我们键入(eq symbol1 'my-symbol)时,读取器使符号MY_SYMBOL驻留,所以当我们试图导入symbol1符号的时候,包里已经存在一个名称相同的符号了。记住,在包里任何时间仅有一个相同名称的符号(这是重点所在),包会维护这种不变式,所以当你试图导入symbol1的时候(它的名字也是MY-SYMBOL),包里已经有一个有相同名字的符号了(正是由读取器悄悄地驻留在里边的)。这种情况叫符号冲突,呃,该问题非常常见。

顺便注意一下,上边的MAKE-SYMBOL的追溯输出的东西看起来出现在S表达式(eq symbol1 'my-symbol)里,这是因为作者本人使用的MCL重新排列了屏幕上的文字,目的是反映事件的真实顺序。因为MAKE-SYMBOL是由读取器调用的,而处理字符串"my-symbol"却是在处理闭合括号之后的,输出看起来就是在那个阶段。如果我们键入:
(list 'x1 'x2 'x3 'x4),输出的结果可能是这样的:

这里,文本格式化被保留下来,目的是有助于表示发生了什么,用户键入的文本是粗体的。(注意大多数Lisp系统都不会做这样的格式化)
#|译者注开始:我的sbcl、ccl并没有出现原作者的格式化,我的输出与普通列表无异,见下边的代码:

CL-USER> (list 'x1 'x2 'x3 'x4)
(X1 X2 X3 X4)
CL-USER> (list 'x1
               'x2
               'x3
               'x4)
(X1 X2 X3 X4)
CL-USER>
|# 译者注完毕

有一些更加重要的函数,我们将继续并提到这些。SYMBOL-NAME返回符号名称的字符串,FIND-SYMBOL带一个字符串参数,告诉你是否存在一个名字为参数值的符号已经驻留,最后,INTERN定义如下:

(defun intern (name)
           (or (find-symbol name)
               (let ((s (make-symbol name)))
                 (import s)
                 s)))
换句话说,INTERN与SYMBOL-NAME有几分相反的意思,SYMBOL-NAME带一个符号作为参数,返回符号名的字符串;INTERN带一个字符串参数,返回名称为该字符串的符号。INTERN就是READ用来创建符号的那个函数。

4. 哪个包?

操作包的函数,像FIND-SYMBOL、IMPORT和INTERN,默认情况下对这样一个包进行操作:它是全局变量*PACHAGE*的值,也就是众所周知的当前包。和符号一样,包也有名字,任意两个包都不会有相同的名字。包与符号一样,也都是普通的Lisp数据结构。这里有两个函数PACKAGE-NAME和FIND-PACLAGE,它们与SYMBOL-NAME和FIND-SYMBOL的操作是相似的,当然也有不同,那个FIND-PACKAGE不带包参数。(这好像是对于包名称有一个全局的元包。)

向我们看到的,我们可以通过调用MAKE-PACKAGE来创建包,通过调用IN-PACKAGE来设置当前包。(注意那个IN-PACKAGE不是一个函数而是宏,它不能对参数求值。)

你可以通过使用特殊语法 ':'来获取那些不在当前包的符号,该语法的意思是包名后边跟着两个冒号,再跟符号名。如果你不想通过导入来使用符号,这种方法就很方便了。例如,如果Bob想使用Jane的FOO函数,他只需要键入JANE::FOO。

4.1 主包

Common Lisp视图维持的一个不变式叫做打印-读取一致性特性。该特性表述为,如果你打印一个符号,然后读取那个符号的结果打印形式,该结果是相同的符号,有两点附加说明:

  1. 该特性不适用于未驻留符号
  2. 该操作仅适用于避免有一定的危险的行为

一会儿我们将解释它的意思。

为了保证打印-读取一致性,一些符号需要与他们的包标识符一起打印。例如:

JANE> (in-package JANE)
#<PACKAGE "JANE">
JANE> 'foo
FOO
JANE> 'jane::foo
FOO
JANE> (in-package "BOB")
#<PACKAGE "BOB">
BOB> 'foo
FOO
BOB> 'jane::foo
JANE::FOO
BOB> 'bob::foo
FOO
BOB>
明显地,一个危险的行为——允许违反打印-读取一致性原则,就是调用IN-PACKAGE。

现在,考虑到下边的情况:

BOB> (in-package :bob)
#<PACKAGE "BOB">
BOB> (unintern 'foo)
T
BOB> (import 'jane::foo)
T
BOB> (make-package :charlie)
#<PACKAGE "CHARLIE">
BOB> (in-package :charlie)
#<COMMON-LISP:PACKAGE "CHARLIE">
CHARLIE> 'bob::foo
JANE::FOO
CHARLIE>
这里我们有一个符号FOO,它即在JANE内,用在BOB包内,因此它既可以在以JANE::FOO,又可以以BOB::FOO里获取。当从CHARLIE包里打印符号的时候(该符号并未在CHARLIE驻留),系统如何选择使用哪种打印形式?

这证明每个符号都保持与一个叫做主包的单个包进行追踪,主包通常是第一个包,那个符号将驻留在那个包里(但是也有例外)。当符号需要使用包标识符打印的时候,使用来自于它的主包的标识符。你可以对它的主包使用SYMBOL-PACKAGE函数来查询符号:

CL-USER> (symbol-package 'bob::foo)
#<PACKAGE "JANE">
CL-USER>
注意,创建符号而没有主包是可能的,例如,未驻留的符号就没有主包。但是不用主包来创建驻留符号也是可能的,例如:
CL-USER> (in-package :jane)
#<PACKAGE "JANE">
JANE> 'weied-symbol
WEIED-SYMBOL
JANE> (in-package BOB)
#<PACKAGE "BOB">
BOB> (import 'jane::weied-symbol)
T
BOB> (in-package :jane)
#<PACKAGE "JANE">
JANE> (unintern 'weied-symbol)
T
JANE> (in-package bob)
#<PACKAGE "BOB">
BOB> 'weied-symbol
#:WEIED-SYMBOL
BOB> (symbol-package 'weied-symbol)
NIL
BOB> (in-package jane)
#<PACKAGE "JANE">
JANE> 'bob::weied-symbol
#:WEIED-SYMBOL
JANE>
这种事情避之则吉。

5. 导出包和使用包

假设Jane和Bob需要在一个软件开发项目上进行合作,每个人都工作在他们自己的包里以避免冲突,Jane写道:

JANE> (in-package jane)
#<PACKAGE "JANE">
JANE> (defclass jane-class () (slot1 slot2 slot3))
#<STANDARD-CLASS JANE-CLASS>
现在,想象一下Bob想要使用jane-class,它写道:
JANE> (in-package bob)
#<PACKAGE "BOB">
BOB> (import 'jane::jane-class)
T
BOB> (make-instance 'jane-class)
#<JANE-CLASS {25418779}>
BOB>
目前为止,一切都好,现在他尝试这么做:
BOB> (setq jc1 (make-instance 'jane-class))
#<JANE-CLASS {25621D99}>
BOB> (slot-value jc1 'slot1)
When attempting to read the slot's value (slot-value), the slot
SLOT1 is missing from the object #<JANE-CLASS {25621D99}>.
   [Condition of type SIMPLE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "new-repl-thread" RUNNING {259323F9}>)
......
又怎么了?好吧,JANE-CLASS定义在JANE包里,所以我们读取的这个槽的名字驻留在JANE包里。但是Bob视图读取那个类的实例,使用的却是BOB包里驻留的符号。换句话说,JANE-CLASS有一个名为JANE::SLOT1的槽,Bob试图读取槽名为BOB::SLOT1的槽,当然了没有这样的槽。

Bob真正要做的是导入与JANE-CLASS相关的所有的符号,那就是说,所有的槽名,方法名等等。他如何知道这些符号是什么呢?他可以查看Jane的代码,再尝试想想办法,但是那也会导致很多问题,至少有一个问题:他可能决定导入一个jane不想让他搞乱的那个符号(记住,从Bob的干预的影响里分离出Jane的符号是使用包的重点所在)。

对Jane来说一个更好的解决方法就是收集一个列表,这个列表里应该导入Bob想要使用Jane的软件而需要的符号,那么Jane就可以这样做:

JANE> (defvar *published-symbols* '(jane-class slot1 slot2 slot3))
*PUBLISHED-SYMBOLS*
JANE>
然后Bob这么做:
(import jane::*published-symbols*)
Common Lisp提供了一个标准的机制来做这件事。每个包都维护一个可能被其它包使用的符号列表,这个列表叫那个包的“输出的符号”的列表,你可以使用EXPORT函数向那个包里增加符号;想要从一个包里移除一个符号,你可以使用UNEXPORT函数(像你期望的,有这个函数);想要向一个包里导入所有已导出的符号,你可以调用USE-PACKAGE;想要解除导入,你可以调用UNUSE-PACKAGE。

关于导出符号,有两件事需要注意:

首先,一个符号可以从任意一个它驻留的包里导出,没必要非是它的主包。为了从一些其它驻留该符号的包里导出,该符号也不需要从其主包导出。

其次,在使用包标识符打印的时候,从其主包导出的符号仅需要使用一个冒号而不需要两个。这就提醒你一个事实:这是一个从主包导出的符号(所以你可能使用USE-PACKAGE对符号的包进行操作,而不是IMPORT符号),同时为了获取它们,通过强制你多键入一个冒号的方式来使用没有导出的符号,会让你很气馁(是的,我没开玩笑)。

6. 屏蔽

还有最后一个你需要知道的事:在包里使用USE-PACKAGE与使用IMPORT导入所有已经导出的符号有一些不同。当你使用IMPORT导入一个符号的时候,你可以使用UNINTERN撤销IMPORT的影响。你却不能使用UNINTERN撤销USE-PACKAGE操作的影响。例如:

BOB> (in-package jane)
#<PACKAGE "JANE">
JANE> (export '(slot1 slot2 slot3))
T
JANE> (in-package bob)
#<PACKAGE "BOB">
BOB> (use-package 'jane)
T
BOB> (symbol-package 'slot1)
#<PACKAGE "JANE">
BOB> (unintern 'slot1)
NIL
BOB> (symbol-package 'slot1)
#<PACKAGE "JANE">
BOB>
这是个问题,因为这使以下这种情况成为可能:使用导出同一个名称的符号的两个不同的包。例如:假设你想在包MYPACKAGE里使用两个包P1和P2,P1和P2都导出了符号名为X的符号。如果你试图使用USE-PACKAGE来使用这两个包的话,你可能会得到名称冲突,因为Lisp没有办法知道MYPACKAGE包里的X是P1:X还是P2:X。

为了解决这样的名字冲突,每个包都维护一个叫做“屏蔽符号列表”的东西,这个屏蔽符号列表是这样的一个符号列表,它屏蔽或覆盖任何正常情况下因调用USE-PACKAGE而在那个包里可见的符号。

向包的屏蔽符号列表里添加符号的方式有两种,SHADOW和SHODOWING-IMPORT。SHADOW用来将包内的符号加入到屏蔽符号列表里;SHADOWING-IMPORT用来将其他包的符号加入到屏蔽符号列表里。

例如:

CL-USER> (make-package :p1 :use '(common-lisp cl-user))
#<PACKAGE "P1">
CL-USER> (make-package :p2 :use '(common-lisp cl-user))
#<PACKAGE "P2">
CL-USER> (in-package p1)
#<PACKAGE "P1">
P1> (export '(x y z))
T
P1> (in-package P2)
#<PACKAGE "P2">
P2> (export '(x y z))
T
P2> (make-package :bob :use '(common-lisp cl-user))
#<PACKAGE "BOB">
P2> (in-package :bob)
#<PACKAGE "BOB">
BOB> (use-package 'p1)
T
BOB> (use-package 'p2)
USE-PACKAGE #<PACKAGE "P2"> causes name-conflicts in
#<PACKAGE "BOB"> between the following symbols:
  P2:Y, P1:Y
   [Condition of type SB-EXT:NAME-CONFLICT]
See also:
  Common Lisp Hyperspec, 11.1.1.2.5 [:section]

Restarts:
 0: [RESOLVE-CONFLICT] Resolve conflict.
 1: [RETRY] Retry SLIME REPL evaluation request.
 2: [*ABORT] Return to SLIME's top level.
 3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {2563FF19}>)

Backtrace:
...........................................
BOB> (unuse-package 'p1)
T
BOB> (shadow 'x)
T
BOB> (shadowing-import 'p1:y)
T
BOB> (shadowing-import 'p2:z)
T
BOB> (use-package 'p1)
T
BOB> (use-package 'p2)
T
BOB> (symbol-package 'x)
#<PACKAGE "BOB">
BOB> (symbol-package 'y)
#<PACKAGE "P1">
BOB> (symbol-package 'z)
#<PACKAGE "P2">
BOB>
为了撤销SHADOW或SHADOWING-IMPORT的影响,可以使用UNINTERN。

注意,UNINTERN(还有很多很多其它令人惊奇的事情)可以导致不期望出现的名称冲突。不期望的名称冲突的最常见的原因通常是在包里不慎地驻留了符号引起的,驻留符号一般是通过在读取器里键入该符号而此时却没有对你所在的包足够关心引起的。

7. DEFPACKAGE宏

既然你已经学过所有用来操作包的大量函数与宏,但你也不应该使用它们中的任意一个,相反地,IMPORT、EXPORT、SHADOW等等的所有功能都会出现在一个单个宏里——DEFPACKAGE宏,它才是你在真实代码里应该用到的。

我不打算给你解释DEFPACKAGE宏,因为既然你已经理解了包的基本概念,你就应该能够读懂hyperspec里的文档并理解它。(hyperspec里还有一些其它的好东西,比如DO-SYMBOLS和WITH-PACKAGE-ITERATOR,现在你应该能够自己理解它们。)

关于使用DEFPACKAGE的一个补充说明:注意传递给DEFPACKAGE的大多数参数都是字符串标识,不是符号,这意味着它们可能是符号,但是如果你选择使用符号,无论此时DEFPACKAGE形式读取到什么,那么那些符号将会驻留在当前包里。这通常导致不受欢迎的结果,养成在你的DEFPACKAGE形式里使用关键字或字符串的习惯是个好主意。

8. 最后的一点想法

理解包的最重要的事情是它们从根本上是Lisp读取器的一部分,而不是求值器的一部分。一旦你的大脑绕明白这个事情,其它的事情就水到渠成了。包控制读取器将字符串映射到符号上的方式(还控制PRINT将符号映射到字符串的方式),没有其它的。尤其需要注意的是,包与函数、值、属性列表等都没有关系,函数、值、属性列表等与特定的符号可能有关系,也可能没关系。

尤其需要注意的是,符号和函数对象都可以被当做函数来使用,但是他们的表现会有些不同,例如:

CL-USER> (defun foo () "Original foo")
FOO
CL-USER> (setf f1 'foo)
FOO
CL-USER> (setf f2 #'foo)
#<FUNCTION FOO>
CL-USER> (defun demo ()
           (list (funcall f1) (funcall f2)
                 (funcall #'foo) (funcall #.#'foo)))
DEMO
CL-USER> (demo)
("Original foo" "Original foo" "Original foo" "Original foo")
CL-USER> (defun foo () "New foo")
STYLE-WARNING: redefining COMMON-LISP-USER::FOO in DEFUN
FOO
CL-USER> (demo)
("New foo" "Original foo" "New foo" "Original foo")
CL-USER>
本例中,我们有两个变量F1和F2,F1的值是符号FOO,F2的值是一个函数对象,在F2被分配的时候,该函数对象在符号FOO的symbol-function槽里。

注意当FOO重定义的时候,调用符号FOO的影响就是获取新的行为,反之调用函数对象就产生老的行为。解释列表里第二各和第四个结果留下当做读取器的练习。

展开阅读全文
加载中

作者的其它热门文章

打赏
2
4 收藏
分享
打赏
2 评论
4 收藏
2
分享
返回顶部
顶部