<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=574&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo-zh / ANSI Common Lisp 第八章：符号]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=574</link>
		<description><![CDATA[ANSI Common Lisp 第八章：符号 最近发表的帖子。]]></description>
		<lastBuildDate>Fri, 18 Nov 2022 13:10:12 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[ANSI Common Lisp 第八章：符号]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=614#p614</link>
			<description><![CDATA[<p>我们一直在使用符号。符号，在看似简单的表面之下，又好像没有那么简单。起初最好不要纠结于背后的实现机制。可以把符号当成数据对象与名字那样使用，而不需要理解两者是如何关联起来的。但到了某个时间点，停下来思考背后是究竟是如何工作会是很有用的。本章解释了背后实现的细节。</p><p>&#160; &#160; &#160; &#160; 8.1 符号名 (Symbol Names)<br />&#160; &#160; &#160; &#160; 8.2 属性列表 (Property Lists)<br />&#160; &#160; &#160; &#160; 8.3 符号很不简单 (Symbols Are Big)<br />&#160; &#160; &#160; &#160; 8.4 创建符号 (Creating Symbols)<br />&#160; &#160; &#160; &#160; 8.5 多重包 (Multiple Packages)<br />&#160; &#160; &#160; &#160; 8.6 关键字 (Keywords)<br />&#160; &#160; &#160; &#160; 8.7 符号与变量 (Symbols and Variables)<br />&#160; &#160; &#160; &#160; 8.8 示例：随机文本 (Example: Random Text)<br />&#160; &#160; &#160; &#160; Chapter 8 总结 (Summary)<br />&#160; &#160; &#160; &#160; Chapter 8 练习 (Exercises)</p><p>8.1 符号名 (Symbol Names)</p><p>第二章描述过，符号是变量的名字，符号本身以对象所存在。但 Lisp 符号的可能性，要比在多数语言仅允许作为变量名来得广泛许多。实际上，符号可以用任何字符串当作名字。可以通过调用 symbol-name 来获得符号的名字：</p><p>&gt; (symbol-name &#039;abc)<br />&quot;ABC&quot;</p><p>注意到这个符号的名字，打印出来都是大写字母。缺省情况下， Common Lisp 在读入时，会把符号名字所有的英文字母都转成大写。代表 Common Lisp 缺省是不分大小写的：</p><p>&gt; (eql &#039;abc &#039;Abc)<br />T<br />&gt; (CaR &#039;(a b c))<br />A</p><p>一个名字包含空白，或其它可能被读取器认为是重要的字符的符号，要用特殊的语法来引用。任何存在垂直杠 (vertical bar)之间的字符序列将被视为符号。可以如下这般在符号的名字中，放入任何字符：</p><p>&gt; (list &#039;|Lisp 1.5| &#039;|| &#039;|abc| &#039;|ABC|)<br />(|Lisp 1.5| || |abc| ABC)</p><p>当这种符号被读入时，不会有大小写转换，而宏字符与其他的字符被视为一般字符。</p><p>那什么样的符号不需要使用垂直杠来参照呢？基本上任何不是数字，或不包含读取器视为重要的字符的符号。一个快速找出你是否可以不用垂直杠来引用符号的方法，是看看 Lisp 如何印出它的。如果 Lisp 没有用垂直杠表示一个符号，如上述列表的最后一个，那么你也可以不用垂直杠。</p><p>记得，垂直杠是一种表示符号的特殊语法。它们不是符号的名字之一：</p><p>&gt; (symbol-name &#039;|a b c|)<br />&quot;a b c&quot;</p><p>(如果想要在符号名称内使用垂直杠，可以放一个反斜线在垂直杠的前面。)</p><p>译注: 反斜线是 \ (backslash)。<br />8.2 属性列表 (Property Lists)</p><p>在 Common Lisp 里，每个符号都有一个属性列表（property-list）或称为 plist 。函数 get 接受符号及任何类型的键值，然后返回在符号的属性列表中，与键值相关的数值：</p><p>&gt; (get &#039;alizarin &#039;color)<br />NIL</p><p>它使用 eql 来比较各个键。若某个特定的属性没有找到时， get 返回 nil 。</p><p>要将值与键关联起来时，你可以使用 setf 及 get :</p><p>&gt; (setf (get &#039;alizarin &#039;color) &#039;red)<br />RED<br />&gt; (get &#039;alizarin &#039;color)<br />RED</p><p>现在符号 alizarin 的 color 属性是 red 。</p><p>../_images/Figure-8.1.png</p><p>图 8.1 符号的结构</p><p>&gt; (setf (get &#039;alizarin &#039;transparency) &#039;high)<br />HIGH<br />&gt; (symbol-plist &#039;alizarin)<br />(TRANSPARENCY HIGH COLOR RED)</p><p>注意，属性列表不以关联列表（assoc-lists）的形式表示，虽然用起来感觉是一样的。</p><p>在 Common Lisp 里，属性列表用得不多。他们大部分被哈希表取代了（4.8 小节）。<br />8.3 符号很不简单 (Symbols Are Big)</p><p>当我们输入名字时，符号就被悄悄地创建出来了，而当它们被显示时，我们只看的到符号的名字。某些情况下，把符号想成是表面所见的东西就好，别想太多。但有时候符号不像看起来那么简单。</p><p>从我们如何使用和检查符号的方式来看，符号像是整数那样的小对象。而符号实际上确实是一个对象，差不多像是由 defstruct 定义的那种结构。符号可以有名字、 主包（home package）、作为变量的值、作为函数的值以及带有一个属性列表。图 8.1 演示了符号在内部是如何表示的。</p><p>很少有程序会使用很多符号，以致于值得用其它的东西来代替符号以节省空间。但需要记住的是，符号是实际的对象，不仅是名字而已。当两个变量设成相同的符号时，与两个变量设成相同列表一样：两个变量的指针都指向同样的对象。<br />8.4 创建符号 (Creating Symbols)</p><p>8.1 节演示了如何取得符号的名字。另一方面，用字符串生成符号也是有可能的。但比较复杂一点，因为我们需要先介绍包（package）。</p><p>概念上来说，包是将名字映射到符号的符号表（symbol-tables）。每个普通的符号都属于一个特定的包。符号属于某个包，我们称为符号被包扣押（intern）了。函数与变量用符号作为名称。包借由限制哪个符号可以访问来实现模块化（modularity），也是因为这样，我们才可以引用到函数与变量。</p><p>大多数的符号在读取时就被扣押了。在第一次输入一个新符号的名字时，Lisp 会产生一个新的符号对象，并将它扣押到当下的包里（缺省是 common-lisp-user 包)。但也可以通过给入字符串与选择性包参数给 intern 函数，来扣押一个名称为字符串名的符号:</p><p>&gt; (intern &quot;RANDOM-SYMBOL&quot;)<br />RANDOM-SYMBOL<br />NIL</p><p>选择性包参数缺省是当前的包，所以前述的表达式，返回当前包里的一个符号，此符号的名字是 “RANDOM-SYMBOL”，若此符号尚未存在时，会创建一个这样的符号出来。第二个返回值告诉我们符号是否存在；在这个情况，它不存在。</p><p>不是所有的符号都会被扣押。有时候有一个自由的（uninterned）符号是有用的，这和公用电话本是一样的原因。自由的符号叫做gensyms 。我们将会在第 10 章讨论宏（Macro）时，理解 gensym 的作用。<br />8.5 多重包 (Multiple Packages)</p><p>大的程序通常切分为多个包。如果程序的每个部分都是一个包，那么开发程序另一个部分的某个人，将可以使用符号来作为函数名或变量名，而不必担心名字在别的地方已经被用过了。</p><p>在没有提供定义多个命名空间的语言里，工作于大项目的程序员，通常需要想出某些规范（convention），来确保他们不会使用同样的名称。举例来说，程序员写显示相关的代码（display code）可能用 disp_ 开头的名字，而写数学相关的代码（math code）的程序员仅使用由 math_ 开始的代码。所以若是数学相关的代码里，包含一个做快速傅立叶转换的函数时，可能会叫做 math_fft 。</p><p>包不过是提供了一种便捷方式来自动办到此事。如果你将函数定义在单独的包里，可以随意使用你喜欢的名字。只有你明确导出（export ）的符号会被别的包看到，而通常前面会有包的名字(或修饰符)。</p><p>举例来说，假设一个程序分为两个包， math 与 disp 。如果符号 fft 被 math 包导出，则 disp 包里可以用 math:fft 来参照它。在 math 包里，可以只用 fft 来参照。</p><p>下面是你可能会放在文件最上方，包含独立包的代码：</p><p>(defpackage &quot;MY-APPLICATION&quot;<br />&#160; &#160; &#160; &#160; &#160; &#160; (:use &quot;COMMON-LISP&quot; &quot;MY-UTILITIES&quot;)<br />&#160; &#160; &#160; &#160; &#160; &#160; (:nicknames &quot;APP&quot;)<br />&#160; &#160; &#160; &#160; &#160; &#160; (:export &quot;WIN&quot; &quot;LOSE&quot; &quot;DRAW&quot;))</p><p>(in-package my-application)</p><p>defpackage 定义一个新的包叫做 my-application[1] 它使用了其他两个包， common-lisp 与 my-utilities ，这代表着可以不需要用包修饰符（package qualifiers）来存取这些包所导出的符号。许多包都使用了 common-lisp 包 ── 因为你不会想给 Lisp 自带的操作符与变量再加上修饰符。</p><p>my-application 包本身只输出三个符号: WIN 、 LOSE 以及 DRAW 。由于调用 defpackage 给了 my-application 一个匿称 app ，则别的包可以这样引用到这些符号，比如 app:win 。</p><p>defpackage 伴随着一个 in-package ，确保当前包是 my-application 。所有其它未修饰的符号会被扣押至 my-application ── 除非之后有别的 in-package 出现。当一个文件被载入时，当前的包总是被重置成载入之前的值。<br />8.6 关键字 (Keywords)</p><p>在 keyword 包的符号 (称为关键字)有两个独特的性质：它们总是对自己求值，以及可以在任何地方引用它们，如 :x 而不是keyword:x 。我们首次在 44 页 (译注: 3.10 小节）介绍关键字参数时， (member &#039;(a) &#039;((a) (z)) test: #&#039;equal) 比 (member&#039;(a) &#039;((a) (z)) :test #&#039;equal) 读起来更自然。现在我们知道为什么第二个较别扭的形式才是对的。 test 前的冒号字首，是关键字的识别符。</p><p>为什么使用关键字而不用一般的符号？因为关键字在哪都可以存取。一个函数接受符号作为实参，应该要写成预期关键字的函数。举例来说，这个函数可以安全地在任何包里调用:</p><p>(defun noise (animal)<br />&#160; (case animal<br />&#160; &#160; (:dog :woof)<br />&#160; &#160; (:cat :meow)<br />&#160; &#160; (:pig :oink)))</p><p>但如果是用一般符号写成的话，它只在被定义的包内正常工作，除非关键字也被导出了。<br />8.7 符号与变量 (Symbols and Variables)</p><p>Lisp 有一件可能会使你困惑的事情是，符号与变量的从两个非常不同的层面互相关联。当符号是特别变量（special variable）的名字时，变量的值存在符号的 value 栏位（图 8.1）。 symbol-value 函数引用到那个栏位，所以在符号与特殊变量的值之间，有直接的连接关系。</p><p>而对于词法变量（lexical variables）来说，事情就完全不一样了。一个作为词法变量的符号只不过是个占位符（placeholder）。编译器会将其转为一个寄存器（register）或内存位置的引用位址。在最后编译出来的代码中，我们无法追踪这个符号 (除非它被保存在调试器「debugger」的某个地方)。因此符号与词法变量的值之间是没有连接的；只要一有值，符号就消失了。<br />8.8 示例：随机文本 (Example: Random Text)</p><p>如果你要写一个操作单词的程序，通常使用符号会比字符串来得好，因为符号概念上是原子性的（atomic）。符号可以用 eql 一步比较完成，而字符串需要使用 string= 或 string-equal 逐一字符做比较。作为一个示例，本节将演示如何写一个程序来产生随机文本。程序的第一部分会读入一个示例文件（越大越好），用来累积之后所给入的相关单词的可能性（likeilhood）的信息。第二部分在每一个单词都根据原本的示例，产生一个随机的权重（weight）之后，随机走访根据第一部分所产生的网络。</p><p>产生的文字将会是部分可信的（locally plausible），因为任两个出现的单词也是输入文件里，两个同时出现的单词。令人惊讶的是，获得看起来是 ── 有意义的整句 ── 甚至整个段落是的频率相当高。</p><p>图 8.2 包含了程序的上半部，用来读取示例文件的代码。</p><p>(defparameter *words* (make-hash-table :size 10000))</p><p>(defconstant maxword 100)</p><p>(defun read-text (pathname)<br />&#160; (with-open-file (s pathname :direction :input)<br />&#160; &#160; (let ((buffer (make-string maxword))<br />&#160; &#160; &#160; &#160; &#160; (pos 0))<br />&#160; &#160; &#160; (do ((c (read-char s nil :eof)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (read-char s nil :eof)))<br />&#160; &#160; &#160; &#160; &#160; ((eql c :eof))<br />&#160; &#160; &#160; &#160; (if (or (alpha-char-p c) (char= c #\&#039;))<br />&#160; &#160; &#160; &#160; &#160; &#160; (progn<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (setf (aref buffer pos) c)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (incf pos))<br />&#160; &#160; &#160; &#160; &#160; &#160; (progn<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (unless (zerop pos)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (see (intern (string-downcase<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(subseq buffer 0 pos))))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (setf pos 0))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (let ((p (punc c)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (if p (see p)))))))))</p><p>(defun punc (c)<br />&#160; (case c<br />&#160; &#160; (#\. &#039;|.|) (#\, &#039;|,|) (#\; &#039;|;|)<br />&#160; &#160; (#\! &#039;|!|) (#\? &#039;|?|) ))</p><p>(let ((prev `|.|))<br />&#160; (defun see (symb)<br />&#160; &#160; (let ((pair (assoc symb (gethash prev *words*))))<br />&#160; &#160; &#160; (if (null pair)<br />&#160; &#160; &#160; &#160; &#160; (push (cons symb 1) (gethash prev *words*))<br />&#160; &#160; &#160; &#160; &#160; (incf (cdr pair))))<br />&#160; &#160; (setf prev symb)))</p><p>图 8.2 读取示例文件</p><p>从图 8.2 所导出的数据，会被存在哈希表 *words* 里。这个哈希表的键是代表单词的符号，而值会像是下列的关联列表（assoc-lists）:</p><p>((|sin| . 1) (|wide| . 2) (|sights| . 1))</p><p>使用弥尔顿的失乐园作为示例文件时，这是与键 |discover| 有关的值。它指出了 “discover” 这个单词，在诗里面用了四次，与 “wide” 用了两次，而 “sin” 与 ”sights” 各一次。(译注: 诗可以在这里找到 <a href="http://www.paradiselost.org/" rel="nofollow">http://www.paradiselost.org/</a> )</p><p>函数 read-text 累积了这个信息。这个函数接受一个路径名（pathname），然后替每一个出现在文件中的单词，生成一个上面所展示的关联列表。它的工作方式是，逐字读取文件的每个字符，将累积的单词存在字符串 buffer 。 maxword 设成 100 ，程序可以读取至多 100 个单词，对英语来说足够了。</p><p>只要下个字符是一个字（由 alpha-char-p 决定）或是一撇 (apostrophe) ，就持续累积字符。任何使单词停止累积的字符会送给see 。数种标点符号（punctuation）也被视为是单词；函数 punc 返回标点字符的伪单词（pseudo-word）。</p><p>函数 see 注册每一个我们看过的单词。它需要知道前一个单词，以及我们刚确认过的单词 ── 这也是为什么要有变量 prev 存在。起初这个变量设为伪单词里的句点；在 see 函数被调用后， prev 变量包含了我们最后见过的单词。</p><p>在 read-text 返回之后， *words* 会包含输入文件的每一个单词的条目（entry）。通过调用 hash-table-count 你可以了解有多少个不同的单词存在。鲜少有英文文件会超过 10000 个单词。</p><p>现在来到了有趣的部份。图 8.3 包含了从图 8.2 所累积的数据来产生文字的代码。 generate-text 函数导出整个过程。它接受一个要产生几个单词的数字，以及选择性传入前一个单词。使用缺省值，会让产生出来的文件从句子的开头开始。</p><p>(defun generate-text (n &amp;optional (prev &#039;|.|))<br />&#160; (if (zerop n)<br />&#160; &#160; &#160; (terpri)<br />&#160; &#160; &#160; (let ((next (random-next prev)))<br />&#160; &#160; &#160; &#160; (format t &quot;~A &quot; next)<br />&#160; &#160; &#160; &#160; (generate-text (1- n) next))))</p><p>(defun random-next (prev)<br />&#160; (let* ((choices (gethash prev *words*))<br />&#160; &#160; &#160; &#160; &#160;(i (random (reduce #&#039;+ choices<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; :key #&#039;cdr))))<br />&#160; &#160; (dolist (pair choices)<br />&#160; &#160; &#160; (if (minusp (decf i (cdr pair)))<br />&#160; &#160; &#160; &#160; &#160; (return (car pair))))))</p><p>图 8.3 产生文字</p><p>要取得一个新的单词， generate-text 使用前一个单词，接著调用 random-next 。 random-next 函数根据每个单词出现的机率加上权重，随机选择伴随输入文本中 prev 之后的单词。</p><p>现在会是测试运行下程序的好时机。但其实你早看过一个它所产生的示例： 就是本书开头的那首诗，是使用弥尔顿的失乐园作为输入文件所产生的。</p><p>(译注: 诗可在这里看，或是浏览书的第 vi 页)</p><p>Half lost on my firmness gains more glad heart,</p><p>Or violent and from forage drives</p><p>A glimmering of all sun new begun</p><p>Both harp thy discourse they match’d,</p><p>Forth my early, is not without delay;</p><p>For their soft with whirlwind; and balm.</p><p>Undoubtedly he scornful turn’d round ninefold,</p><p>Though doubled now what redounds,</p><p>And chains these a lower world devote, yet inflicted?</p><p>Till body or rare, and best things else enjoy’d in heav’n</p><p>To stand divided light at ev’n and poise their eyes,</p><p>Or nourish, lik’ning spiritual, I have thou appear.</p><p>── Henley<br />Chapter 8 总结 (Summary)</p><p>&#160; &#160; 符号的名字可以是任何字符串，但由 read 创建的符号缺省会被转成大写。<br />&#160; &#160; 符号带有相关联的属性列表，虽然他们不需要是相同的形式，但行为像是 assoc-lists 。<br />&#160; &#160; 符号是实质的对象，比较像结构，而不是名字。<br />&#160; &#160; 包将字符串映射至符号。要在包里给符号创造一个条目的方法是扣留它。符号不需要被扣留。<br />&#160; &#160; 包通过限制可以引用的名称增加模块化。缺省的包会是 user 包，但为了提高模块化，大的程序通常分成数个包。<br />&#160; &#160; 可以让符号在别的包被存取。关键字是自身求值并在所有的包里都可以存取。<br />&#160; &#160; 当一个程序用来操作单词时，用符号来表示单词是很方便的。</p><p>Chapter 8 练习 (Exercises)</p><p>&#160; &#160; 可能有两个同名符号，但却不 eql 吗？<br />&#160; &#160; 估计一下用字符串表示 “FOO” 与符号表示 foo 所使用内存空间的差异。<br />&#160; &#160; 只使用字符串作为实参 来调用 137 页的 defpackage 。应该使用符号比较好。为什么使用字符串可能比较危险呢？<br />&#160; &#160; 加入需要的代码，使图 7.1 的代码可以放在一个叫做 &quot;RING&quot; 的包里，而图 7.2 的代码放在一个叫做 &quot;FILE&quot; 包里。不需要更动现有的代码。<br />&#160; &#160; 写一个确认引用的句子是否是由 Henley 生成的程序 (8.8 节)。<br />&#160; &#160; 写一版 Henley，接受一个单词，并产生一个句子，该单词在句子的中间。</p><p>脚注</p><p>[1] | 调用 defpackage 里的名字全部大写的缘故在 8.1 节提到过，符号的名字缺省被转成大写。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 18 Nov 2022 13:10:12 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=614#p614</guid>
		</item>
	</channel>
</rss>
