我们一直在使用符号。符号,在看似简单的表面之下,又好像没有那么简单。起初最好不要纠结于背后的实现机制。可以把符号当成数据对象与名字那样使用,而不需要理解两者是如何关联起来的。但到了某个时间点,停下来思考背后是究竟是如何工作会是很有用的。本章解释了背后实现的细节。

8.1 符号名 (Symbol Names)

[ 第二章描述过,符号是变量的名字,符号本身以对象所存在。但 Lisp 符号的可能性,要比在多数语言仅允许作为变量名来得广泛许多。实际上,符号可以用任何字符串当作名字。可以通过调用 symbol-name 来获得符号的名字:

  1. > (symbol-name 'abc)
  2. "ABC"

注意到这个符号的名字,打印出来都是大写字母。缺省情况下, Common Lisp 在读入时,会把符号名字所有的英文字母都转成大写。代表 Common Lisp 缺省是不分大小写的:

  1. > (eql 'abc 'Abc)
  2. T
  3. > (CaR '(a b c))
  4. A

一个名字包含空白,或其它可能被读取器认为是重要的字符的符号,要用特殊的语法来引用。任何存在垂直杠 (vertical bar)之间的字符序列将被视为符号。可以如下这般在符号的名字中,放入任何字符:

  1. > (list '|Lisp 1.5| '|| '|abc| '|ABC|)
  2. (|Lisp 1.5| || |abc| ABC)

当这种符号被读入时,不会有大小写转换,而宏字符与其他的字符被视为一般字符。

那什么样的符号不需要使用垂直杠来参照呢?基本上任何不是数字,或不包含读取器视为重要的字符的符号。一个快速找出你是否可以不用垂直杠来引用符号的方法,是看看 Lisp 如何印出它的。如果 Lisp 没有用垂直杠表示一个符号,如上述列表的最后一个,那么你也可以不用垂直杠。

记得,垂直杠是一种表示符号的特殊语法。它们不是符号的名字之一:

  1. > (symbol-name '|a b c|)
  2. "a b c"

(如果想要在符号名称内使用垂直杠,可以放一个反斜线在垂直杠的前面。)

译注: 反斜线是 \ (backslash)。 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#symbol-names)

8.2 属性列表 (Property Lists)

[ 在 Common Lisp 里,每个符号都有一个属性列表(property-list)或称为 plist 。函数 get 接受符号及任何类型的键值,然后返回在符号的属性列表中,与键值相关的数值:

  1. > (get 'alizarin 'color)
  2. NIL

它使用 eql 来比较各个键。若某个特定的属性没有找到时, get 返回 nil

要将值与键关联起来时,你可以使用 setfget :

  1. > (setf (get 'alizarin 'color) 'red)
  2. RED
  3. > (get 'alizarin 'color)
  4. RED

现在符号 alizarincolor 属性是 red

../_images/Figure-8.1.png

图 8.1 符号的结构

  1. > (setf (get 'alizarin 'transparency) 'high)
  2. HIGH
  3. > (symbol-plist 'alizarin)
  4. (TRANSPARENCY HIGH COLOR RED)

注意,属性列表不以关联列表(assoc-lists)的形式表示,虽然用起来感觉是一样的。

在 Common Lisp 里,属性列表用得不多。他们大部分被哈希表取代了(4.8 小节)。 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#property-lists)

8.3 符号很不简单 (Symbols Are Big)

[ 当我们输入名字时,符号就被悄悄地创建出来了,而当它们被显示时,我们只看的到符号的名字。某些情况下,把符号想成是表面所见的东西就好,别想太多。但有时候符号不像看起来那么简单。

从我们如何使用和检查符号的方式来看,符号像是整数那样的小对象。而符号实际上确实是一个对象,差不多像是由 defstruct 定义的那种结构。符号可以有名字、 主包(home package)、作为变量的值、作为函数的值以及带有一个属性列表。图 8.1 演示了符号在内部是如何表示的。

很少有程序会使用很多符号,以致于值得用其它的东西来代替符号以节省空间。但需要记住的是,符号是实际的对象,不仅是名字而已。当两个变量设成相同的符号时,与两个变量设成相同列表一样:两个变量的指针都指向同样的对象。 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#symbols-are-big)

8.4 创建符号 (Creating Symbols)

[ 8.1 节演示了如何取得符号的名字。另一方面,用字符串生成符号也是有可能的。但比较复杂一点,因为我们需要先介绍包(package)。

概念上来说,包是将名字映射到符号的符号表(symbol-tables)。每个普通的符号都属于一个特定的包。符号属于某个包,我们称为符号被包扣押(intern)了。函数与变量用符号作为名称。包借由限制哪个符号可以访问来实现模块化(modularity),也是因为这样,我们才可以引用到函数与变量。

大多数的符号在读取时就被扣押了。在第一次输入一个新符号的名字时,Lisp 会产生一个新的符号对象,并将它扣押到当下的包里(缺省是 common-lisp-user 包)。但也可以通过给入字符串与选择性包参数给 intern 函数,来扣押一个名称为字符串名的符号:

  1. > (intern "RANDOM-SYMBOL")
  2. RANDOM-SYMBOL
  3. NIL

选择性包参数缺省是当前的包,所以前述的表达式,返回当前包里的一个符号,此符号的名字是 “RANDOM-SYMBOL”,若此符号尚未存在时,会创建一个这样的符号出来。第二个返回值告诉我们符号是否存在;在这个情况,它不存在。

不是所有的符号都会被扣押。有时候有一个自由的(uninterned)符号是有用的,这和公用电话本是一样的原因。自由的符号叫做gensyms 。我们将会在第 10 章讨论宏(Macro)时,理解 gensym 的作用。 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#creating-symbols)

8.5 多重包 (Multiple Packages)

[ 大的程序通常切分为多个包。如果程序的每个部分都是一个包,那么开发程序另一个部分的某个人,将可以使用符号来作为函数名或变量名,而不必担心名字在别的地方已经被用过了。

在没有提供定义多个命名空间的语言里,工作于大项目的程序员,通常需要想出某些规范(convention),来确保他们不会使用同样的名称。举例来说,程序员写显示相关的代码(display code)可能用 disp_ 开头的名字,而写数学相关的代码(math code)的程序员仅使用由 math_ 开始的代码。所以若是数学相关的代码里,包含一个做快速傅立叶转换的函数时,可能会叫做 math_fft

包不过是提供了一种便捷方式来自动办到此事。如果你将函数定义在单独的包里,可以随意使用你喜欢的名字。只有你明确导出(export )的符号会被别的包看到,而通常前面会有包的名字(或修饰符)。

举例来说,假设一个程序分为两个包, mathdisp 。如果符号 fftmath 包导出,则 disp 包里可以用 math:fft 来参照它。在 math 包里,可以只用 fft 来参照。

下面是你可能会放在文件最上方,包含独立包的代码:

  1. (defpackage "MY-APPLICATION"
  2. (:use "COMMON-LISP" "MY-UTILITIES")
  3. (:nicknames "APP")
  4. (:export "WIN" "LOSE" "DRAW"))
  5. (in-package my-application)

](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#multiple-packages) defpackage 定义一个新的包叫做 my-application [1] 它使用了其他两个包, common-lispmy-utilities ,这代表着可以不需要用包修饰符(package qualifiers)来存取这些包所导出的符号。许多包都使用了 common-lisp 包 ── 因为你不会想给 Lisp 自带的操作符与变量再加上修饰符。

my-application 包本身只输出三个符号: WINLOSE 以及 DRAW 。由于调用 defpackage 给了 my-application 一个匿称 app ,则别的包可以这样引用到这些符号,比如 app:win

defpackage 伴随着一个 in-package ,确保当前包是 my-application 。所有其它未修饰的符号会被扣押至 my-application ── 除非之后有别的 in-package 出现。当一个文件被载入时,当前的包总是被重置成载入之前的值。

8.6 关键字 (Keywords)

[ 在 keyword 包的符号 (称为关键字)有两个独特的性质:它们总是对自己求值,以及可以在任何地方引用它们,如 :x 而不是keyword:x 。我们首次在 44 页 (译注: 3.10 小节)介绍关键字参数时, (member '(a) '((a) (z)) test: #'equal)(member'(a) '((a) (z)) :test #'equal) 读起来更自然。现在我们知道为什么第二个较别扭的形式才是对的。 test 前的冒号字首,是关键字的识别符。

为什么使用关键字而不用一般的符号?因为关键字在哪都可以存取。一个函数接受符号作为实参,应该要写成预期关键字的函数。举例来说,这个函数可以安全地在任何包里调用:

  1. (defun noise (animal)
  2. (case animal
  3. (:dog :woof)
  4. (:cat :meow)
  5. (:pig :oink)))

但如果是用一般符号写成的话,它只在被定义的包内正常工作,除非关键字也被导出了。 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#keywords)

8.7 符号与变量 (Symbols and Variables)

[ Lisp 有一件可能会使你困惑的事情是,符号与变量的从两个非常不同的层面互相关联。当符号是特别变量(special variable)的名字时,变量的值存在符号的 value 栏位(图 8.1)。 symbol-value 函数引用到那个栏位,所以在符号与特殊变量的值之间,有直接的连接关系。

而对于词法变量(lexical variables)来说,事情就完全不一样了。一个作为词法变量的符号只不过是个占位符(placeholder)。编译器会将其转为一个寄存器(register)或内存位置的引用位址。在最后编译出来的代码中,我们无法追踪这个符号 (除非它被保存在调试器「debugger」的某个地方)。因此符号与词法变量的值之间是没有连接的;只要一有值,符号就消失了。 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#symbols-and-variables)

8.8 示例:随机文本 (Example: Random Text)

[ 如果你要写一个操作单词的程序,通常使用符号会比字符串来得好,因为符号概念上是原子性的(atomic)。符号可以用 eql 一步比较完成,而字符串需要使用 string=string-equal 逐一字符做比较。作为一个示例,本节将演示如何写一个程序来产生随机文本。程序的第一部分会读入一个示例文件(越大越好),用来累积之后所给入的相关单词的可能性(likeilhood)的信息。第二部分在每一个单词都根据原本的示例,产生一个随机的权重(weight)之后,随机走访根据第一部分所产生的网络。

产生的文字将会是部分可信的(locally plausible),因为任两个出现的单词也是输入文件里,两个同时出现的单词。令人惊讶的是,获得看起来是 ── 有意义的整句 ── 甚至整个段落是的频率相当高。

图 8.2 包含了程序的上半部,用来读取示例文件的代码。

  1. (defparameter *words* (make-hash-table :size 10000))
  2. (defconstant maxword 100)
  3. (defun read-text (pathname)
  4. (with-open-file (s pathname :direction :input)
  5. (let ((buffer (make-string maxword))
  6. (pos 0))
  7. (do ((c (read-char s nil :eof)
  8. (read-char s nil :eof)))
  9. ((eql c :eof))
  10. (if (or (alpha-char-p c) (char= c #\'))
  11. (progn
  12. (setf (aref buffer pos) c)
  13. (incf pos))
  14. (progn
  15. (unless (zerop pos)
  16. (see (intern (string-downcase
  17. (subseq buffer 0 pos))))
  18. (setf pos 0))
  19. (let ((p (punc c)))
  20. (if p (see p)))))))))
  21. (defun punc (c)
  22. (case c
  23. (#\. '|.|) (#\, '|,|) (#\; '|;|)
  24. (#\! '|!|) (#\? '|?|) ))
  25. (let ((prev `|.|))
  26. (defun see (symb)
  27. (let ((pair (assoc symb (gethash prev *words*))))
  28. (if (null pair)
  29. (push (cons symb 1) (gethash prev *words*))
  30. (incf (cdr pair))))
  31. (setf prev symb)))

图 8.2 读取示例文件

从图 8.2 所导出的数据,会被存在哈希表 *words* 里。这个哈希表的键是代表单词的符号,而值会像是下列的关联列表(assoc-lists):

  1. ((|sin| . 1) (|wide| . 2) (|sights| . 1))

](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#example-random-text) 使用弥尔顿的失乐园作为示例文件时,这是与键 |discover| 有关的值。它指出了 “discover” 这个单词,在诗里面用了四次,与 “wide” 用了两次,而 “sin” 与 ”sights” 各一次。(译注: 诗可以在这里找到 http://www.paradiselost.org/ )

函数 read-text 累积了这个信息。这个函数接受一个路径名(pathname),然后替每一个出现在文件中的单词,生成一个上面所展示的关联列表。它的工作方式是,逐字读取文件的每个字符,将累积的单词存在字符串 buffermaxword 设成 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 函数导出整个过程。它接受一个要产生几个单词的数字,以及选择性传入前一个单词。使用缺省值,会让产生出来的文件从句子的开头开始。

  1. (defun generate-text (n &optional (prev '|.|))
  2. (if (zerop n)
  3. (terpri)
  4. (let ((next (random-next prev)))
  5. (format t "~A " next)
  6. (generate-text (1- n) next))))
  7. (defun random-next (prev)
  8. (let* ((choices (gethash prev *words*))
  9. (i (random (reduce #'+ choices
  10. :key #'cdr))))
  11. (dolist (pair choices)
  12. (if (minusp (decf i (cdr pair)))
  13. (return (car pair))))))

图 8.3 产生文字

要取得一个新的单词, generate-text 使用前一个单词,接著调用 random-nextrandom-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)

[

  • 符号的名字可以是任何字符串,但由 read 创建的符号缺省会被转成大写。
  • 符号带有相关联的属性列表,虽然他们不需要是相同的形式,但行为像是 assoc-lists 。
  • 符号是实质的对象,比较像结构,而不是名字。
  • 包将字符串映射至符号。要在包里给符号创造一个条目的方法是扣留它。符号不需要被扣留。
  • 包通过限制可以引用的名称增加模块化。缺省的包会是 user 包,但为了提高模块化,大的程序通常分成数个包。
  • 可以让符号在别的包被存取。关键字是自身求值并在所有的包里都可以存取。
  • 当一个程序用来操作单词时,用符号来表示单词是很方便的。

    ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#chapter-8-summary)

Chapter 8 练习 (Exercises)

[

  • 可能有两个同名符号,但却不 eql 吗?
  • 估计一下用字符串表示 “FOO” 与符号表示 foo 所使用内存空间的差异。
  • 只使用字符串作为实参 来调用 137 页的 defpackage 。应该使用符号比较好。为什么使用字符串可能比较危险呢?
  • 加入需要的代码,使图 7.1 的代码可以放在一个叫做 "RING" 的包里,而图 7.2 的代码放在一个叫做 "FILE" 包里。不需要更动现有的代码。
  • 写一个确认引用的句子是否是由 Henley 生成的程序 (8.8 节)。
  • 写一版 Henley,接受一个单词,并产生一个句子,该单词在句子的中间。

脚注 ](http://acl.readthedocs.org/en/latest/zhCN/ch8-cn.html#chapter-8-exercises) [1] | 调用 defpackage 里的名字全部大写的缘故在 8.1 节提到过,符号的名字缺省被转成大写。