首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

Practical Common Lisp札记

2012-10-10 
Practical Common Lisp笔记断断续续看了很久了,也没看完的书。现在据说要出中文版了,打算直接买本中文版的

Practical Common Lisp笔记

断断续续看了很久了,也没看完的书。

现在据说要出中文版了,打算直接买本中文版的看看。

emacs muse发布的版本:pcl.zip

?

环境搭建

整理CD

在REPL中将显示每次添加新记录后的返回值。

查看数据库内容

但更好的办法是写一个dump-db函数以便于阅读的格式显示出数据库:

这个函数使用DOLIST宏遍历*db*中的元素,将每个元素依次绑定到变量cd。并使用FORMAT函数打印cd。

FORMAT接收至少两个参数,第一个是它要输出的流;t是*standard-output*的简写形式。

第二个参数是一个格式字符串,它包含纯文本和告诉FORMAT函数如何显示其它参数的转义字符。转义字符以~开头(与printf中的%类似)。FORMAT函数有许多的转义字符。在这里只了解编写dump-db需要的转义符。

~a是一个美化转义符;它消耗掉一个参数并将它以便于阅读的形式输出。这将使输出不包含:和引号。例如:

~t转义符表示缩进。~10t告诉FORMAT在处理下一个~a前缩进足够的空间到第10列。另外,~t不消耗任何参数。

当FORMAT遇到~{时下个被消耗的参数必须是一个list。FORMAT将遍历这个list,以~{和~}间的转义符处理list中的每个元素。~%转义符不消耗任何参数但它使得FORMAT进行换行。~}之前的那个~%在输出数据库的一个字段后换行。~}之后的那个~%在输出一条记录后换行。

技术上来说,可以让FORMAT自己来遍历数据库,将dump-db函数写为一行。

非常cool。

改进用户交互

使用FORMAT提示输入。调用FORCE-OUTPUT确保提示显示出来。

使用READ-LINE函数读取单行文本。*query-io*是一个全局变量,它包含连接的终端的输入。prompt-read的返回值是最后一个form的值,READ-LINE返回读取的字符串(不包含换行符)。

可以结合make-cd和prompt-read构建一个函数根据输入来构建一个新的CD记录。

读取数据库的函数与写入的函数是相类似的。

将整个表达式封装到函数中,这个函数只需要artist参数:

这看起来仍然不美观。我们可以将匿名函数的创建封装起来。

现在我们需要更多的函数来生成选择器函数。但这又将导致重复代码的产生,并且它们不能处理组合查询条件。我们需要一个函数根据字段或多个字段生成选择器函数。为实现这个目标,我们需要先了解keyword parameter。

通常的函数结构如下:

应该修改为:

上面的macro是如何工作的呢?当REPL开始对backwards语句求值时,它识别出backwards是一个宏的名字。因此它不对表达式("hello, world" t format)求值,这很好因为它不是一个合法Lisp form。然后它将这个list传到backwards的代码中。backwards中的代码将这个list传递给REVERSE函数,它返回新的list (format t "hello, world")。backwards将这个值返回给REPL,然后它被进行求值。

backwards macro定义了一个新的语言它与Lisp类似只是逆序的。对于一个已经编译过的Lisp程序,这个新语言效率上与原始的Lisp同样有效因为所有的macro代码——用于生成新表达式的代码——在编译时被执行。换言之,编译器将对(backwards ("hello, world" t format))和(format t "hello, world")生成相同的代码。

在数据库示例的选择器函数生成器中。我们可以编写macro来根据特殊的参数生成所需要的函数。我们手工针对某个字段的优化代码如下:

产生的变量将包含下面的list。

这个list被传递到make-comparisons-list中,它返回包含比较表达式的list。可以使用函数MACROEXPAND-1来查看将产生什么样的调用。如果传递给MACROEXPAND-1的form是macro,它将使用适当的参数调用macro代码并返回展开的表达式。例如:

语法和语义关于括号

另一个创建绑定的方式是LET*。后面定义的变量可以引用前面定义的变量的值。

LET是如何知道绑定*x*是一个动态绑定的呢?这是因为在申明*x*是使用的是DEFVAR和DEFPARAMETER,它们会自动将变量申明为全局特殊变量。在任何时候在LET中或在函数参数中使用此变量的时候都将构造一个新的动态绑定。这也是为什么使用*标识全局变量这个约定的非常重要的原因。

也可以申明本地特殊变量。在binding form中,申明一个特殊变量,则将创建一个动态绑定的变量而不是lexical变量。其它代码可以在本地变量范围中申明一个特殊变量以便引用动态绑定。但是,本地特殊变量非常稀有,你不必考虑它们。

动态绑定使得全局变量更加易于管理,绑定全局变量考虑它可能的两个影响:它将改变下游代码的行为,也可能出现下游代码给绑定赋新值断开了现有的绑定行为。只应该在需要的地方使用上面的这些特性。

常量

它表示对大于等于0小于等于19的素数执行body中的代码,变量p保存对应的素数。

在没有do-primes宏时,需要使用下面的DO循环:

接下来需要编写宏代码将前面的示例代码转换为上面的代码。

宏参数

它也指出,宏有3种情况将产生漏洞。

当前的宏定义有其中的一种漏洞:命名漏洞,它对end表达式进行了多次求值。所设在调用do-primes时不直接使用数字而是使用(random 100)替代19:

它展开后将变成:

当这段展开的代码运行时,RANDOM将在每次循环时都被执行。这个循环将只会在随机数字恰好小于P时才会中止。迭代执行的次数也将是随机的。

这是宏抽像中的漏洞。一个解决这个漏洞的办法是简单的说明do-primes的行为。但这不满足最小惊讶原则。程序员通常会认为他传递给宏的表达式只会被求值一次。而且,do-primes是构建在标准宏DOTIMES和DOLIST的模型的基础上,这两者都只对表达式求值一次,绝大多数程序员都会认为do-primes会有相似的行为。

可以在生成的代码中对表达式求值并保存于变量中放在后面的DO循环中使用。

现在可以看到每个测试的情况了。但是这里有很多重复的FORMAT调用,需要进行重构。

另一个问题是对于整个测试过程没有返回一个标识来标识是否所有测试都通过。

重构

重新编写的test-+,提升并不大,但是至少可以方便的修改结果报告的格式。

现在,看起来似乎只需要将PROGN修改为AND就可以了。但是AND有个问题,因为它有短路行为。如果某个测试用例失败,后面的测试用例将不会被执行。Common Lisp也没有提供这样的控制结构。因此我们需要编写一个宏提供这样的功能。它的行为类似于:

这样修改后再运行test-arithmetic将变成:

值得回顾一下得到上面的代码的整个过程,因为它描绘出了通常情况下怎样编写Lisp程序。

我们开始于为这个问题定义一个简单的版本——如何执行一堆布尔类型的表达式并检查它们是否都返回true。这只需要使用AND将它们连接起来,但是还需要更好的显示测试结果。所以我们又编写了一些充满重复和容易出错的代码能以我们需要的方式显示测试结果。

下一步我们重构了第二个版本得到了简洁的代码。首先是使用标准的重构技术将一些代码放到了函数report-result中。不幸的是,我们发现使用report-result时仍然是乏味且容易出错的,因为需要传递两次测试表达式,一次用于得到表达式的值另一次是将表达式本身作为数据。因此我们又编写了check宏自动生成对report-result调用的细节。

当编写check时,我们意识到可以在单次check调用中可以生成多次对report-result的调用,这让我们回到了最初的最精简的使用AND的版本。

这时check API已经固定下来,因为你可以直接修改它的内部来影响它的工作方式。下一个任务是修正代码让它返回一个值来标识整个测试是否全部通过。我们没有立即来hacking check方法,而是幻想设计一个小型的语言。幻想有一个没有AND的短路行为的控制结构。但是我们意识到没有那样的结构,但是我们可以编写一个这样的结构。编写完combine-results后,才发现对check的修正确实是微不足道的。

现在只需要提升显示测试结果了。开始修改test函数时,我们认识到这些函数代表了一个特殊的函数分类,它们应该要有自己的抽象。因此我们编写了deftest来抽象测试函数的编码模式。

deftest提供了抽象测试定义和下层机构间的抽象屏障,我们可以提升测试结果的显示而不需要动测试函数。

数字、字符和字符串集合

-IF-IF-NOT与前面讨论的那些不带后缀的函数接收相同的关键字参数除了::test参数不需要,因为带后缀的函数的第一个参数就是这个函数。:key参数,它用于将从序列中得到的元素转换为传递给:test函数的参数。

REMOVE族的函数还支持第四种变种,REMOVE-DUPLICATES,它只需要一个sequence参数,它会从中删除所有重复的元素。它与REMOVE函数接收相同的关键字参数,但是:count除外,因为它总是会移除重复的元素。

排序和合并

这两个函数不同之处在于STABLE-SORT保证不会重新排序相等的元素,而SORT有可能会对相等的元素重新排序。

这些函数是“破坏性”函数的例子。破坏性函数允许——通常是基于效率原因——以任意方法或多或少的修改它们的参数。这有两个含义:其一,你总是应该对这些函数的返回值做某些处理(比如将它赋值给变量或将它传递到其它的函数),其二,除非你对将要传递给破坏性函数的参数已经操作完毕了,否则你应该传递它的拷贝。

通常在排序后你不会关心未排序的版本,因此有意允许SORT和STABLE-SORT在排序过程中销毁sequence。这意味着你需要记得将代码写成下面的样子:

SUBSEQ也是SETFable的,但是它不会扩展或缩短sequence;如果新值和将被替换的subsequence的长度不同,较短的那个将决定实际将替换多少个。

为了查找出两个sequence是从哪里开始有不同元素的可以使用MISMATCH函数。它接收两个sequence并返回第一个不匹配的元素的索引。

如果字符串匹配它将返回NIL。MISMATCH也可以接收许多标准的关键字参数:key参数指定提取进行比较的值的函数;:test参数指定比较函数;:start1:end1:start2:end2用于指定两个sequence中的subsequence。:from-end参数为T时表示应该从后面开始搜索,这时将返回从后面开始查找直到遇到不同元素时的在第一个sequence中的索引。

Sequence Predicates

由于设置某个key对应的值为NIL后,这个key仍然会存在于table中,因此你需要另一个函数将键值对完全从hash table中删除。REMHASH接收GETHASH相同的参数并将移除相应的键值对。你也可以使用CLRHASH来清空hash table中的所有键值对。

Hash Table迭代

在迭代过程中向hash table中添加或删除元素的结果不明确(通常会是不好的):你可以使用SETF和GETHASH修改当前entry的值,使用REMHASH移除当前的entry。例如,移除所有值小于10的entries,可以写成:

叫作LISP的原因:List Processing

因为ASSOC是从头至尾搜索list的,因此一个key/value对可以屏蔽另一个拥有相同key的key/value对。

CL-USER> (assoc 'a '((a . 10) (a . 1) (b . 2) (c . 3)))(A . 10)

可以使用CONS将key/value对加到alist的前面。

(cons (cons 'new-key 'new-value) alist)

Common Lisp也提供了ACONS函数:

(acons 'new-key 'new-value alist)

与CONS一样,ACONS函数不会修改这个alist。如果你需要修改它,则需要写成下面的方式:

(setf alist (acons 'new-key 'new-value alist))

(push (cons 'new-key 'new-value) alist)

显然,使用ASSOC搜索所花的时间依赖于匹配的键值对在list中的深度。由于alist的机制是非常轻量级的,对于小的tablesalist可以用过hash table。在如何查找方面alist也提供了更多弹性。可以使用ASSOC-IF和ASSOC-IF-NOT函数来查找,它们返回第一个使用test函数与CAR匹配的键值对(ASSOC-IF-NOT表示不匹配)RASSOC、RASSOC-IF和RASSOC-IF-NOT与ASSOC类函数类似只是它们执行的是逆序的查找。

COPY-ALIST与COPY-TREE类似,只是它不复制整个tree结构,它只复制构成list结构的cons cells和这些cons cells的CAR直接引用的cons cells。即原alist和复制的alist都包含相同的键值对象,即使这些键值刚好是由cons cells构成。

你可以使用PAIRLIS函数将两个分别作为键和值的list构造成alist。

CL-USER> (pairlis '(a b c) '(1 2 3))((C . 3) (B . 2) (A . 1))

也可能得到

CL-USER> (pairlis '(a b c) '(1 2 3))((A . 1) (B . 2) (C . 3))

另一种look table是plist。结构上来讲plist只是标准的list。将A B和C映射为1 2 3的plist结构如下:

Practical Common Lisp札记

但是,plist的弹性比alist小些。实际上,plist只支持一个基础的GETF查找操作。GETF函数接收一个plist和key作为参数并返回相应的vaue或者在未找到时返回NIL。

与ASSOC不同,GETF总是使用EQ比较key是否匹配。因此,你不应该使用数字或字符作为plist的key;这些类型的EQ行为是不确定的。实际上应该总是使用符号来作为plist的key。

可以使用SETF和GETF来设置或取key相关的value。

CL-USER> (defparameter *plist* ())*PLIST*CL-USER> *plist*NILCL-USER> (setf (getf *plist* :a) 1)1CL-USER> *plist*(:A 1)CL-USER> (setf (getf *plist* :a) 2)2CL-USER> *plist*(:A 2)

使用REMF宏从plist中移除键值对。如果找到了对应的key它返回true。

与GETF类似REMF也是使用EQ来比较key的。

由于plist经常用于从同一个plist中提取多个不同的属性,Common Lisp提供了GET-PROPERTIES函数,这使得它从单个plist中提取多个属性时更有效率。它接收一个plist和一个包含key的list作为参数搜索并返回多个值,它一次查找多个值,可以避免多次扫描plist。

plist与符号的关系:每个符号对象都有与之相关联的plist可以用于保存与符号相关的信息。可以使用函数SYMBOL-PLIST获得这个plist。但我们通常很少关心整个plist,更常见的是使用GET函数,它接收一个符号和一个key,它等同于使用GETF和SYMBOL-PLIST。

(get 'symbol 'key) === (getf (symbol-plist 'symbol) 'key)

与GETF类似,GET也是SETFable的,因此你可以向符号添加任意信息:

(setf (get 'some-symbol 'my-key) "information")

可以使用REMF和SYMBOL-PLIST来移除属性或者使用REMPROP。

(remprop 'symbol 'key) === (remf (symbol-plist 'symbol key))
DESTRUCTURING-BIND

DESTRUCTURING-BIND宏是用于切割lists的。这个宏提供了一个方法destructure任意lists,类似于宏的参数列表可以接收它的list参数的一部分。它的基本结构如下:

(destructuring-bind (parameter*) list  body-form*)

参数parameter list可以包含任何类型的参数,例如:&optional,&rest和&key参数。在parameter lists中任何参数都可以被替换为嵌套的destructuring参数list,它接收list的一部分。list form将被求值一次并应该返回一个list,然后被destructured并将对应的值绑定到parameter指定的变量中。例如:

(destructuring-bind (x y z) (list 1 2 3)  (list :x x :y y :z z)) ==> (:X 1 :Y 2 :Z 3)(destructuring-bind (x y z) (list 1 (list 2 20) 3)  (list :x x :y y :z z)) ==> (:X 1 :Y (2 20) :Z 3)(destructuring-bind (x (y1 y2) z) (list 1 (list 2 20) 3)  (list :x x :y1 y1 :y2 y2 :z z)) ==> (:X 1 :Y1 2 :Y2 20 :Z 3)(destructuring-bind (x (y1 &optional y2) z) (list 1 (list 2 20) 3)  (list :x x :y1 y1 :y2 y2 :z z)) ==> (:X 1 :Y1 2 :Y2 20 :Z 3)(destructuring-bind (x (y1 &optional y2) z) (list 1 (list 2) 3)  (list :x x :y1 y1 :y2 y2 :z z)) ==> (:X 1 :Y1 2 :Y2 NIL :Z 3)(destructuring-bind (&key x y z) (list :x 1 :y 2 :z 3)  (list :x x :y y :z z)) ==> (:X 1 :Y 2 :Z 3)(destructuring-bind (&key x y z) (list :z 1 :y 2 :x 3)  (list :x x :y y :z z)) ==> (:X 3 :Y 2 :Z 1)

有一种类型的参数可以用于DESTRUCTURING-BIND也可以用于宏参数列表,即&whole参数。如果指定了这种类型的,则它必须是参数列表中的第一个,它将被绑定到整个list form。在&whole参数后面,其它参数可以按正常情况出现就像&whole参数不存在一样。例如:

(destructuring-bind (&whole whole &key x y z) (list :z 1 :y 2 :x 3)  (list :x x :y y :z z :whole whole))==> (:X 3 :Y 2 :Z 1 :WHOLE (:Z 1 :Y 2 :X 3))
异常处理:conditions和restarts

多数语言中错误的处理方式是通过返回值和异常机制。

这两种机制的缺点在于,当调用栈的底层出现问题,想要进行其进行修复时会由于堆栈已经被清除,用来恢复错误的代码将无法获得出问题之前的上下文环境而出现问题。

例如,一个三层的调用,如果底层调用失败,中间层不能修复,控制权传到了高层代码。高层代码在处理错误时,它只能有两个项:在没有中层和底层协助的情况下来完成修复或者做出某种修改,以使得再次调用中层代码时能成功。第一个选项需要高层代码实现许多额外的工作——实现整个中间层所做的操作。并且由于堆栈被恢复了,会有更多的工作需要做。第二个选项则需要进行某种修补和重新调用——即改变某些东西使得再次调用底层代码时不会产生错误,这需要高层代码了解中间层和底层代码的工作方式,这与将函数当作黑盒处理的理念是相冲突的。

Lisp的处理方式

Common Lisp的处理方式是将恢复错误的代码与决定如何恢复的代码分离。可以将恢复错误的代码放在底层函数中而不执行任何实际的恢复策略,将这个决定权留给高层函数。

Conditions

类似于异常,可以用DEFINE-CONDITION宏来定义,与定义DEFCLASS类似,它默认的父类为CONDITION而不是STANDARD-OBJECT。Slots的指定与类定义相似。但是condition的slots不能使用SLOT-VALUE访问;必须为你需要使用的值指定:reader或:accessor。使用MAKE-CONDITION定义新的condition。使用参数:initargs来初始化slots,没有类似INITIALIZE-INSTANCE的机制来初始化。

使用condition系统处理错误时,应该将condition定义为ERROR的子类,它是CONDITION的子类。

热点排行