附錄: 包(packages)

2018-02-24 15:54 更新

附錄: 包(packages)

包(packages),是 Common Lisp 把代碼組織成模塊的方式。早期的 Lisp 方言有一張符號(hào)表,即oblist【注1】。在這張表里列出了系統(tǒng)中所有已經(jīng)讀取到的符號(hào)。借助 oblist 里的符號(hào)表項(xiàng),系統(tǒng)得以存取數(shù)據(jù),諸如對象的值,以及屬性列表等。保存在 oblist 里的符號(hào)被稱為 interned。

新一些的 Lisp 方言把?oblist?的概念放到了一個(gè)個(gè)包里面。現(xiàn)在,符號(hào)不僅僅是被?intern?了,而是被?intern?在某個(gè)包里。包之所以支持模塊化是因?yàn)樵谝粋€(gè)包里的?intern?的符號(hào)只有在其被顯式聲明為能被其它包訪問的時(shí)候,它才能為外部訪問(除非用一些歪門邪道的招數(shù))。

包是一種 Lisp 對象。當(dāng)前包常常被保存在一個(gè)名為?\*package\*?的全局變量里面。當(dāng) Common Lisp 啟動(dòng)時(shí),當(dāng)前包就是用戶包:或者叫 user (CLTL1?實(shí)現(xiàn)),或者叫?common-lisp-user?(CLTL2實(shí)現(xiàn))。

包一般用自己的名字相互區(qū)別,而這些名字采用的是字符串的形式。要知道當(dāng)前包的包名,可以試試:

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

通常,當(dāng)讀入一個(gè)符號(hào)時(shí),它就被?intern?到當(dāng)前的包里了。要弄清給定符號(hào)所?intern?的是哪個(gè)包,我們可以

用?symbol-package?:

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

這個(gè)返回值是實(shí)際的包對象。為便于將來使用,我們給?foo?賦一個(gè)值:

> (setq foo 99)
99

使用?in-package?,我們就可以切換到另一個(gè)新的包,若有需要的話這個(gè)包會(huì)被創(chuàng)建出來【注2】:

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

此時(shí)此刻應(yīng)該會(huì)響起詭異的背景音樂,因?yàn)槲覀円呀?jīng)身處另一個(gè)世界:在這里?foo?已經(jīng)不似從前了:

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

為什么會(huì)這樣?因?yàn)橹氨晃覀冊O(shè)置成?99?的那個(gè)?foo?和現(xiàn)在?mine?里面的這個(gè) foo 是兩碼事?!咀?】要從用戶包之外引用原來的這個(gè)?foo?,我們必須把包名和兩個(gè)冒號(hào)作為它的前綴:

MINE> common-lisp-user::foo
99

因此,具有相同打印名稱的不同符號(hào)得以在不同包中共存。這樣就可以在名為?common-lisp-user?的包里有一個(gè)?foo?,同時(shí)在?mine?包里也有一個(gè)?foo?,并且它們兩個(gè)是不一樣的符號(hào)。實(shí)際上,這就是?package?的一部分用意所在,即:你在為你的函數(shù)和變量取名字的同時(shí),就不用擔(dān)心別人會(huì)把一樣的名字用在其它東西上。現(xiàn)在,就算有重名的情況,重名的符號(hào)之間也是互不相干的。

與此同時(shí),包也提供了一種信息隱藏的手段。對程序來說,它必須使用名字來引用不同的函數(shù)和變量。如果你不讓一個(gè)名字在你的包之外可見的話,那么另一個(gè)包中的代碼就無法使用或者修改這個(gè)名字所引用的對象。

在寫程序的時(shí)候,把包的名字帶上兩個(gè)冒號(hào)做為前綴并不是個(gè)好習(xí)慣。你要是這樣做的話,就違背了模塊化設(shè)計(jì)的初衷,而這正是包機(jī)制的本意。如果你不得不使用雙冒號(hào)來引用一個(gè)符號(hào),這應(yīng)該就是有人根本就不希望你引用它。

一般來說,你只應(yīng)該引用那些被 export 了的符號(hào)。把符號(hào)從它所屬的包 export 出來,我們就能讓這個(gè)符號(hào)對其它包變得可見。要導(dǎo)出一個(gè)符號(hào),我們可以調(diào)用(你肯定已經(jīng)猜到了) export :

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

現(xiàn)在,如果回到了?mine?包,那么就可以用一個(gè)冒號(hào)引用?bar?,因?yàn)檫@個(gè)名字是外部可見的:

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

如果把?bar?import?到?mine?里面,我們就能更進(jìn)一步,讓?mine?能和?user?包共享?bar?這個(gè)符號(hào):

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

在導(dǎo)入?bar?之后,我們可以根本不用加任何包的限定符,就能引用它了。現(xiàn)在,這兩個(gè)包共享了同一個(gè)符號(hào) -- 再?zèng)]有一個(gè)獨(dú)立的?mine:bar?了。

萬一已經(jīng)有了一個(gè)會(huì)怎么樣呢?在這種情況下,import?調(diào)用會(huì)導(dǎo)致一個(gè)錯(cuò)誤,就像下面我們試著import?foo?時(shí)造成的錯(cuò)誤一樣:

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

之前,我們在?mine?里對?foo?進(jìn)行了一次不成功的求值,這次求值順帶著使得一個(gè)名為 foo 的符號(hào)被加入了?mine?。由于這個(gè)符號(hào)在全局范圍內(nèi)還沒有值,因此產(chǎn)生了一個(gè)錯(cuò)誤,但是輸入符號(hào)名字的直接后果就是使它被 intern 進(jìn)了這個(gè)包。所以,當(dāng)我們現(xiàn)在想把?foo?引進(jìn)?mine?的時(shí)候,mine里面已經(jīng)有一個(gè)相同名字的符號(hào)了。

通過讓一個(gè)包使用 (use) 另一個(gè)包,我們也能批量的引入符號(hào):

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

這樣,所有?user?package?引出的符號(hào)就會(huì)自動(dòng)地被引進(jìn)到?mine?里面去了。(要是?user``package?已經(jīng)引出了?foo?的話,這個(gè)函數(shù)調(diào)用也會(huì)出一個(gè)錯(cuò)。)

根據(jù) CLTL2,包含內(nèi)建操作符和變量名字的包被稱為?common-lisp?而不是?lisp?,因此新一些的包在缺省情況下已不再使用?lisp?包了。由于我們通過調(diào)用in-package?創(chuàng)建了?mine?,而在這次調(diào)用中也?use?了這個(gè)包,所以所有?Common Lisp?的名字在?mine?中都是可見的:

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

在實(shí)際的編程中,你不得不讓所有新編寫的包使用 common-lisp (或者其他某個(gè)含 Lisp 操作符的包)。否則你甚至?xí)]辦法跳出這個(gè)新的包?!咀?】

一般來說,在編譯后的代碼中,不會(huì)像剛才這樣在頂層進(jìn)行包的操作。更多的時(shí)候,這些關(guān)于包的函數(shù)調(diào)用會(huì)被包含在源文件中。通常,只要把 in-package 和 defpackage 放在源文件的開頭就可以了。

(defpackage 宏是?CLTL2?里新引進(jìn)的,但是有些較老的實(shí)現(xiàn)也提供了它。) 如果你要編寫一個(gè)獨(dú)立的包,下面列出了你可能會(huì)放在對應(yīng)的源文件最開始地方的代碼:

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

這會(huì)使得該文件里所有的代碼,或者更準(zhǔn)確地說,文件里所有的名字,都納入了?my-application?這個(gè)包。

my-application?同時(shí)使用了?common-lisp?和?my-utilities?,因此,不用加任何包名作為前綴,所有被引出的符號(hào)都可以直接使用。

my-application?本身僅僅引出了三個(gè)符號(hào),它們分別是:win、lose?和?draw?。由于在調(diào)用in-package?的時(shí)候,我們給?my-application?取了一個(gè)綽號(hào)?app?,在其它包里面的代碼可以用類似?app:win?的名字來引用這些符號(hào)。

像這樣的用包來提供的模塊化的確有點(diǎn)不自然。我們的包里面不是對象,而是一堆名字。每個(gè)使用common-lisp?的包都引入了?cons?這個(gè)名字,原因在于?common-lisp?包含了一個(gè)叫這個(gè)名字的函數(shù)。但是,這樣會(huì)導(dǎo)致一個(gè)名字叫?cons?的變量也在每個(gè)使用?common-lisp?的程序里可見。這樣的事情同樣也會(huì)在?Common Lisp?的其他名字空間重演。如果包(package) 這個(gè)機(jī)制讓你頭痛,那么這就是一個(gè)最主要的原因 -- 包不是基于對象而是基于名字。

和包相關(guān)的操作會(huì)發(fā)生在讀取時(shí)(read-time),而非運(yùn)行時(shí)。這可能會(huì)造成一些困擾。我們輸入的第二個(gè)表達(dá)式:

(symbol-package 'foo)

之所以會(huì)返回它返回的那個(gè)值是因?yàn)椋鹤x取這個(gè)查詢語句的同時(shí),答案就被生成了。為了求值這個(gè)表達(dá)式,Lisp?必須先讀入它,這意味著要?intern?foo。

再來個(gè)例子,看看下面把兩個(gè)表達(dá)式交換順序的結(jié)果,這兩個(gè)表達(dá)式前面曾出現(xiàn)過:

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

通常來說,在頂層輸入兩個(gè)表達(dá)式的效果等價(jià)于把這兩個(gè)表達(dá)式放在一個(gè)progn 里面。不過這次有些不同。如果我們這樣說

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

則會(huì)得到個(gè)錯(cuò)誤提示。錯(cuò)誤的原因在于?progn?表達(dá)式在求值之前就已經(jīng)被 read 處理過了。當(dāng)調(diào)用 read 時(shí),當(dāng)前包還是 mine ,因而 bar 被認(rèn)為是 mine:bar 。運(yùn)行這個(gè)表達(dá)式的效果就好像我們想要從 user 包 export 出 mine:bar ,而不是從?common-lisp-user?export 出?common-lisp-user:bar?一樣。

package?被如此定義,使得編寫那些把符號(hào)當(dāng)作數(shù)據(jù)的程序成為一樁麻煩事。舉個(gè)例子,要是像下面那樣定義 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)))

這樣的話,如果我們從另外一個(gè)包調(diào)用 noise ,同時(shí)傳進(jìn)去的參數(shù)是不認(rèn)識(shí)的符號(hào),noise 會(huì)走到 case 語句的末尾,并返回 nil :

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

這是因?yàn)閭鬟M(jìn)去的參數(shù)是?common-lisp-user:pig?(這沒有冒犯閣下的意思),然而?case?接受?key是?other:pig?。為了讓?noise?像我們期望的那樣工作,就必須把里面用到的所有六個(gè)符號(hào)都引出來,再在調(diào)用 noise 的包里面引入它們。

在此例中,我們也可以通過使用關(guān)鍵字而不是常規(guī)的符號(hào),來繞過這個(gè)問題。倘若 noise 像下面這樣定義:

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

的話,我們就能從任意一個(gè)包安全地調(diào)用這個(gè)函數(shù)了:

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

關(guān)鍵字就像金子:普適而且自身就能表明其價(jià)值。不論在哪里它們都是可見的,而且它們從不需要被引用。

在編寫類似?defanaph?( 16.3 節(jié)) 的符號(hào)驅(qū)動(dòng)的函數(shù)時(shí),基本上應(yīng)該總是用關(guān)鍵字參數(shù)。

包里面有很多地方讓人不解。這里對這一主題的介紹不過是冰山一角。要知道所有的細(xì)節(jié),請參考CLTL2?的第 11 章。

備注:

【注1】譯者注:GNU Emacs 和 XEmacs 使用的是一張名為 obarray 的哈希表。

【注2】在較早期的 Common Lisp 實(shí)現(xiàn)下,請省略掉 :use 參數(shù)

【注3】有的 Common Lisp 實(shí)現(xiàn)會(huì)在 toplevel 提示符的前面顯示包的名字。這個(gè)特性不是必須的,但的確是比較貼心的設(shè)計(jì)。

【注4】譯者注:即你不僅沒有辦法使用cons ,更糟糕的是,你也不能用in-package 切換到其它包。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)