我們一直在使用符號。符號,在看似簡單的表面之下,又好像沒有那么簡單。起初最好不要糾結于背后的實現機制。可以把符號當成數據對象與名字那樣使用,而不需要理解兩者是如何關聯起來的。但到了某個時間點,停下來思考背后是究竟是如何工作會是很有用的。本章解釋了背后實現的細節。
[TOC]
## 8.1 符號名 (Symbol Names)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#symbol-names "Permalink to this headline")
第二章描述過,符號是變量的名字,符號本身以對象所存在。但 Lisp 符號的可能性,要比在多數語言僅允許作為變量名來得廣泛許多。實際上,符號可以用任何字符串當作名字。可以通過調用?`symbol-name`?來獲得符號的名字:
~~~
> (symbol-name 'abc)
"ABC"
~~~
注意到這個符號的名字,打印出來都是大寫字母。缺省情況下, Common Lisp 在讀入時,會把符號名字所有的英文字母都轉成大寫。代表 Common Lisp 缺省是不分大小寫的:
~~~
> (eql 'abc 'Abc)
T
> (CaR '(a b c))
A
~~~
一個名字包含空白,或其它可能被讀取器認為是重要的字符的符號,要用特殊的語法來引用。任何存在垂直杠 (vertical bar)之間的字符序列將被視為符號。可以如下這般在符號的名字中,放入任何字符:
~~~
> (list '|Lisp 1.5| '|| '|abc| '|ABC|)
(|Lisp 1.5| || |abc| ABC)
~~~
當這種符號被讀入時,不會有大小寫轉換,而宏字符與其他的字符被視為一般字符。
那什么樣的符號不需要使用垂直杠來參照呢?基本上任何不是數字,或不包含讀取器視為重要的字符的符號。一個快速找出你是否可以不用垂直杠來引用符號的方法,是看看 Lisp 如何印出它的。如果 Lisp 沒有用垂直杠表示一個符號,如上述列表的最后一個,那么你也可以不用垂直杠。
記得,垂直杠是一種表示符號的特殊語法。它們不是符號的名字之一:
~~~
> (symbol-name '|a b c|)
"a b c"
~~~
(如果想要在符號名稱內使用垂直杠,可以放一個反斜線在垂直杠的前面。)
譯注: 反斜線是?`\`?(backslash)。
## 8.2 屬性列表 (Property Lists)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#property-lists "Permalink to this headline")
在 Common Lisp 里,每個符號都有一個屬性列表(property-list)或稱為?`plist`?。函數?`get`?接受符號及任何類型的鍵值,然后返回在符號的屬性列表中,與鍵值相關的數值:
~~~
> (get 'alizarin 'color)
NIL
~~~
它使用?`eql`?來比較各個鍵。若某個特定的屬性沒有找到時,?`get`?返回?`nil`?。
要將值與鍵關聯起來時,你可以使用?`setf`?及?`get`?:
~~~
> (setf (get 'alizarin 'color) 'red)
RED
> (get 'alizarin 'color)
RED
~~~
現在符號?`alizarin`?的?`color`?屬性是?`red`?。

**圖 8.1 符號的結構**
~~~
> (setf (get 'alizarin 'transparency) 'high)
HIGH
> (symbol-plist 'alizarin)
(TRANSPARENCY HIGH COLOR RED)
~~~
注意,屬性列表不以關聯列表(assoc-lists)的形式表示,雖然用起來感覺是一樣的。
在 Common Lisp 里,屬性列表用得不多。他們大部分被哈希表取代了(4.8 小節)。
## 8.3 符號很不簡單 (Symbols Are Big)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#symbols-are-big "Permalink to this headline")
當我們輸入名字時,符號就被悄悄地創建出來了,而當它們被顯示時,我們只看的到符號的名字。某些情況下,把符號想成是表面所見的東西就好,別想太多。但有時候符號不像看起來那么簡單。
從我們如何使用和檢查符號的方式來看,符號像是整數那樣的小對象。而符號實際上確實是一個對象,差不多像是由?`defstruct`?定義的那種結構。符號可以有名字、 主包(home package)、作為變量的值、作為函數的值以及帶有一個屬性列表。圖 8.1 演示了符號在內部是如何表示的。
很少有程序會使用很多符號,以致于值得用其它的東西來代替符號以節省空間。但需要記住的是,符號是實際的對象,不僅是名字而已。當兩個變量設成相同的符號時,與兩個變量設成相同列表一樣:兩個變量的指針都指向同樣的對象。
## 8.4 創建符號 (Creating Symbols)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#creating-symbols "Permalink to this headline")
8.1 節演示了如何取得符號的名字。另一方面,用字符串生成符號也是有可能的。但比較復雜一點,因為我們需要先介紹包(package)。
概念上來說,包是將名字映射到符號的符號表(symbol-tables)。每個普通的符號都屬于一個特定的包。符號屬于某個包,我們稱為符號被包扣押(intern)了。函數與變量用符號作為名稱。包借由限制哪個符號可以訪問來實現模塊化(modularity),也是因為這樣,我們才可以引用到函數與變量。
大多數的符號在讀取時就被扣押了。在第一次輸入一個新符號的名字時,Lisp 會產生一個新的符號對象,并將它扣押到當下的包里(缺省是?`common-lisp-user`?包)。但也可以通過給入字符串與選擇性包參數給?`intern`?函數,來扣押一個名稱為字符串名的符號:
~~~
> (intern "RANDOM-SYMBOL")
RANDOM-SYMBOL
NIL
~~~
選擇性包參數缺省是當前的包,所以前述的表達式,返回當前包里的一個符號,此符號的名字是 “RANDOM-SYMBOL”,若此符號尚未存在時,會創建一個這樣的符號出來。第二個返回值告訴我們符號是否存在;在這個情況,它不存在。
不是所有的符號都會被扣押。有時候有一個自由的(uninterned)符號是有用的,這和公用電話本是一樣的原因。自由的符號叫做*gensyms*?。我們將會在第 10 章討論宏(Macro)時,理解?`gensym`?的作用。
## 8.5 多重包 (Multiple Packages)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#multiple-packages "Permalink to this headline")
大的程序通常切分為多個包。如果程序的每個部分都是一個包,那么開發程序另一個部分的某個人,將可以使用符號來作為函數名或變量名,而不必擔心名字在別的地方已經被用過了。
在沒有提供定義多個命名空間的語言里,工作于大項目的程序員,通常需要想出某些規范(convention),來確保他們不會使用同樣的名稱。舉例來說,程序員寫顯示相關的代碼(display code)可能用?`disp_`?開頭的名字,而寫數學相關的代碼(math code)的程序員僅使用由?`math_`?開始的代碼。所以若是數學相關的代碼里,包含一個做快速傅立葉轉換的函數時,可能會叫做?`math_fft`?。
包不過是提供了一種便捷方式來自動辦到此事。如果你將函數定義在單獨的包里,可以隨意使用你喜歡的名字。只有你明確導出(`export`?)的符號會被別的包看到,而通常前面會有包的名字(或修飾符)。
舉例來說,假設一個程序分為兩個包,?`math`?與?`disp`?。如果符號?`fft`?被?`math`?包導出,則?`disp`?包里可以用?`math:fft`?來參照它。在?`math`?包里,可以只用?`fft`?來參照。
下面是你可能會放在文件最上方,包含獨立包的代碼:
~~~
(defpackage "MY-APPLICATION"
(:use "COMMON-LISP" "MY-UTILITIES")
(:nicknames "APP")
(:export "WIN" "LOSE" "DRAW"))
(in-package my-application)
~~~
`defpackage`?定義一個新的包叫做?`my-application`?[[1]](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#id4)?它使用了其他兩個包,?`common-lisp`?與?`my-utilities`?,這代表著可以不需要用包修飾符(package qualifiers)來存取這些包所導出的符號。許多包都使用了?`common-lisp`?包 ── 因為你不會想給 Lisp 自帶的操作符與變量再加上修飾符。
`my-application`?包本身只輸出三個符號:?`WIN`?、?`LOSE`?以及?`DRAW`?。由于調用?`defpackage`?給了?`my-application`?一個匿稱?`app`?,則別的包可以這樣引用到這些符號,比如?`app:win`?。
`defpackage`?伴隨著一個?`in-package`?,確保當前包是?`my-application`?。所有其它未修飾的符號會被扣押至?`my-application`?── 除非之后有別的?`in-package`?出現。當一個文件被載入時,當前的包總是被重置成載入之前的值。
## 8.6 關鍵字 (Keywords)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#keywords "Permalink to this headline")
在?`keyword`?包的符號 (稱為關鍵字)有兩個獨特的性質:它們總是對自己求值,以及可以在任何地方引用它們,如?`:x`?而不是`keyword:x`?。我們首次在 44 頁 (譯注: 3.10 小節)介紹關鍵字參數時,?`(member?'(a)?'((a)?(z))?test:?#'equal)`?比?`(member'(a)?'((a)?(z))?:test?#'equal)`?讀起來更自然。現在我們知道為什么第二個較別扭的形式才是對的。?`test`?前的冒號字首,是關鍵字的識別符。
為什么使用關鍵字而不用一般的符號?因為關鍵字在哪都可以存取。一個函數接受符號作為實參,應該要寫成預期關鍵字的函數。舉例來說,這個函數可以安全地在任何包里調用:
~~~
(defun noise (animal)
(case animal
(:dog :woof)
(:cat :meow)
(:pig :oink)))
~~~
但如果是用一般符號寫成的話,它只在被定義的包內正常工作,除非關鍵字也被導出了。
## 8.7 符號與變量 (Symbols and Variables)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#symbols-and-variables "Permalink to this headline")
Lisp 有一件可能會使你困惑的事情是,符號與變量的從兩個非常不同的層面互相關聯。當符號是特別變量(special variable)的名字時,變量的值存在符號的 value 欄位(圖 8.1)。?`symbol-value`?函數引用到那個欄位,所以在符號與特殊變量的值之間,有直接的連接關系。
而對于詞法變量(lexical variables)來說,事情就完全不一樣了。一個作為詞法變量的符號只不過是個占位符(placeholder)。編譯器會將其轉為一個寄存器(register)或內存位置的引用位址。在最后編譯出來的代碼中,我們無法追蹤這個符號 (除非它被保存在調試器「debugger」的某個地方)。因此符號與詞法變量的值之間是沒有連接的;只要一有值,符號就消失了。
## 8.8 示例:隨機文本 (Example: Random Text)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#example-random-text "Permalink to this headline")
如果你要寫一個操作單詞的程序,通常使用符號會比字符串來得好,因為符號概念上是原子性的(atomic)。符號可以用?`eql`?一步比較完成,而字符串需要使用?`string=`?或?`string-equal`?逐一字符做比較。作為一個示例,本節將演示如何寫一個程序來產生隨機文本。程序的第一部分會讀入一個示例文件(越大越好),用來累積之后所給入的相關單詞的可能性(likeilhood)的信息。第二部分在每一個單詞都根據原本的示例,產生一個隨機的權重(weight)之后,隨機走訪根據第一部分所產生的網絡。
產生的文字將會是部分可信的(locally plausible),因為任兩個出現的單詞也是輸入文件里,兩個同時出現的單詞。令人驚訝的是,獲得看起來是 ── 有意義的整句 ── 甚至整個段落是的頻率相當高。
圖 8.2 包含了程序的上半部,用來讀取示例文件的代碼。
~~~
(defparameter *words* (make-hash-table :size 10000))
(defconstant maxword 100)
(defun read-text (pathname)
(with-open-file (s pathname :direction :input)
(let ((buffer (make-string maxword))
(pos 0))
(do ((c (read-char s nil :eof)
(read-char s nil :eof)))
((eql c :eof))
(if (or (alpha-char-p c) (char= c #\'))
(progn
(setf (aref buffer pos) c)
(incf pos))
(progn
(unless (zerop pos)
(see (intern (string-downcase
(subseq buffer 0 pos))))
(setf pos 0))
(let ((p (punc c)))
(if p (see p)))))))))
(defun punc (c)
(case c
(#\. '|.|) (#\, '|,|) (#\; '|;|)
(#\! '|!|) (#\? '|?|) ))
(let ((prev `|.|))
(defun see (symb)
(let ((pair (assoc symb (gethash prev *words*))))
(if (null pair)
(push (cons symb 1) (gethash prev *words*))
(incf (cdr pair))))
(setf prev symb)))
~~~
**圖 8.2 讀取示例文件**
從圖 8.2 所導出的數據,會被存在哈希表?`*words*`?里。這個哈希表的鍵是代表單詞的符號,而值會像是下列的關聯列表(assoc-lists):
~~~
((|sin| . 1) (|wide| . 2) (|sights| . 1))
~~~
使用[彌爾頓的失樂園](http://zh.wikipedia.org/wiki/%E5%A4%B1%E6%A8%82%E5%9C%92)作為示例文件時,這是與鍵?`|discover|`?有關的值。它指出了 “discover” 這個單詞,在詩里面用了四次,與 “wide” 用了兩次,而 “sin” 與 ”sights” 各一次。(譯注: 詩可以在這里找到?[http://www.paradiselost.org/](http://www.paradiselost.org/)?)
函數?`read-text`?累積了這個信息。這個函數接受一個路徑名(pathname),然后替每一個出現在文件中的單詞,生成一個上面所展示的關聯列表。它的工作方式是,逐字讀取文件的每個字符,將累積的單詞存在字符串?`buffer`?。?`maxword`?設成?`100`?,程序可以讀取至多 100 個單詞,對英語來說足夠了。
只要下個字符是一個字(由?`alpha-char-p`?決定)或是一撇 (apostrophe) ,就持續累積字符。任何使單詞停止累積的字符會送給`see`?。數種標點符號(punctuation)也被視為是單詞;函數?`punc`?返回標點字符的偽單詞(pseudo-word)。
函數?`see`?注冊每一個我們看過的單詞。它需要知道前一個單詞,以及我們剛確認過的單詞 ── 這也是為什么要有變量?`prev`?存在。起初這個變量設為偽單詞里的句點;在?`see`?函數被調用后,?`prev`?變量包含了我們最后見過的單詞。
在?`read-text`?返回之后,?`*words*`?會包含輸入文件的每一個單詞的條目(entry)。通過調用?`hash-table-count`?你可以了解有多少個不同的單詞存在。鮮少有英文文件會超過 10000 個單詞。
現在來到了有趣的部份。圖 8.3 包含了從圖 8.2 所累積的數據來產生文字的代碼。?`generate-text`?函數導出整個過程。它接受一個要產生幾個單詞的數字,以及選擇性傳入前一個單詞。使用缺省值,會讓產生出來的文件從句子的開頭開始。
~~~
(defun generate-text (n &optional (prev '|.|))
(if (zerop n)
(terpri)
(let ((next (random-next prev)))
(format t "~A " next)
(generate-text (1- n) next))))
(defun random-next (prev)
(let* ((choices (gethash prev *words*))
(i (random (reduce #'+ choices
:key #'cdr))))
(dolist (pair choices)
(if (minusp (decf i (cdr pair)))
(return (car pair))))))
~~~
**圖 8.3 產生文字**
要取得一個新的單詞,?`generate-text`?使用前一個單詞,接著調用?`random-next`?。?`random-next`?函數根據每個單詞出現的機率加上權重,隨機選擇伴隨輸入文本中?`prev`?之后的單詞。
現在會是測試運行下程序的好時機。但其實你早看過一個它所產生的示例: 就是本書開頭的那首詩,是使用彌爾頓的失樂園作為輸入文件所產生的。
(譯注: 詩可在這里看,或是瀏覽書的第 vi 頁)
Half lost on my firmness gains more glad heart,
Or violent and from forage drives
A glimmering of all sun new begun
Both harp thy discourse they match’d,
Forth my early, is not without delay;
For their soft with whirlwind; and balm.
Undoubtedly he scornful turn’d round ninefold,
Though doubled now what redounds,
And chains these a lower world devote, yet inflicted?
Till body or rare, and best things else enjoy’d in heav’n
To stand divided light at ev’n and poise their eyes,
Or nourish, lik’ning spiritual, I have thou appear.
── Henley
## Chapter 8 總結 (Summary)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#chapter-8-summary "Permalink to this headline")
1. 符號的名字可以是任何字符串,但由?`read`?創建的符號缺省會被轉成大寫。
2. 符號帶有相關聯的屬性列表,雖然他們不需要是相同的形式,但行為像是 assoc-lists 。
3. 符號是實質的對象,比較像結構,而不是名字。
4. 包將字符串映射至符號。要在包里給符號創造一個條目的方法是扣留它。符號不需要被扣留。
5. 包通過限制可以引用的名稱增加模塊化。缺省的包會是 user 包,但為了提高模塊化,大的程序通常分成數個包。
6. 可以讓符號在別的包被存取。關鍵字是自身求值并在所有的包里都可以存取。
7. 當一個程序用來操作單詞時,用符號來表示單詞是很方便的。
## Chapter 8 練習 (Exercises)[](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#chapter-8-exercises "Permalink to this headline")
1. 可能有兩個同名符號,但卻不?`eql`?嗎?
2. 估計一下用字符串表示 “FOO” 與符號表示 foo 所使用內存空間的差異。
3. 只使用字符串作為實參 來調用 137 頁的?`defpackage`?。應該使用符號比較好。為什么使用字符串可能比較危險呢?
4. 加入需要的代碼,使圖 7.1 的代碼可以放在一個叫做?`"RING"`?的包里,而圖 7.2 的代碼放在一個叫做?`"FILE"`?包里。不需要更動現有的代碼。
5. 寫一個確認引用的句子是否是由 Henley 生成的程序 (8.8 節)。
6. 寫一版 Henley,接受一個單詞,并產生一個句子,該單詞在句子的中間。
腳注
[[1]](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#id2) | 調用?`defpackage`?里的名字全部大寫的緣故在 8.1 節提到過,符號的名字缺省被轉成大寫。