1 背景知识

1.1 我为什么写这样一个介绍材料?

这份材料的初稿是在2002年八月完成的,所以其中的描述,并不完全反应我现在的情况的情况。但是为了保持最初的原味,我决定不去修改它们。

我现在的情况是这样的:过去的7年里,如果项目所需要的Java扩展已经存在,我就使用 Java。更往前算,我主要使用Wirth家族的语言(主要是Modula-2和Oberon)。所以,一开始我对Java相对其他语言而言的优点感到非常满意。

在过去的一年里,我逐渐意识到,Java依然是一种能力非常(实际上是极端)有限的语言,所以我开始寻找一个可能的替代方案。因为我参与了Feyerabend项目(http://www.dreamsongs.com/Feyerabend/Feyerabend.html), Lisp自然的成为候选之一。(Richard Gabriel启动了Feyerabend项目,而他也是1980年代初期推动Common Lisp标准化进程的那群人中的一个。)

虽 然,有很多非常漂亮的编程语言存在(比如,按照字母顺序:gbeta,Objective CAML, Python, Ruby);但我很快得到这样的印象:某种程度上来说,Lisp是所有语言的源头。这样说的主要理由,是Lisp 通过统一了代码(code)和数据(data)形成了一套完成的计算理论(比”仅仅”图林完全更加强大。更多的信息,参见下面”Lisp的技术背景 ”一节)。这意味着(理论上),Lisp没有给你任何限制:如果你用其他任何一种语言可以完成某项工作,Lisp中也可以。更进一步,Lisp在运行期 检测所有类型,所以没有静态类型系统来碍你的事。

总结这些观察,你能够得到这样的结论:Lisp的主旨就是:表达能力是语言唯一重要的属性。当你希望使用这种能力的时候,不应该有任何阻碍。编程语言不应该将自己对世界的看法强加给程序员。应当由程序员来让编程语言适应自己的需要,而不是通过其他的途径。

这 对我有非常大的吸引力,所以我决定使用Lisp。然而,当你更认真的考察Lisp的时候,你会发现Lisp有一个缺陷:整个网络上没有对这门语言好的介绍 -至少我没有找到。当然,还是有一些介绍存在,但是他们或者太浅显,只针对非常初级的程序员;或者太学术,只涉及到一些理论上令人感兴趣的问题。而我希望 看到的,是提供足够的背景材料,能够尽快帮助入门的介绍(某种可以称为”给资深程序员的Lisp介绍”的东西)。

因为我没有找到任何介绍,我不得不自己浏览所有能找到的资料。现在,我要呈现的,就是我自己的总结,希望对后来者有所帮助。

为 什么把这篇介绍称为”极端片面”呢?Lisp是一门非常复杂的语言,或者可以成为一组语言。对于Lisp来说不可能存在一种”权威指南”这样的东 西。我在这里展现的,是我自己希望在最初就看到的。其他人可能会有不同的想法。而我并不准备写一篇适合于每一个人,涉及每一个细节的文章。我只是要想一些 人,比如我自己,提供一些有用的信息。因为这些资料对我来说有价值,我想它们对其他人应该也会有用。

特别的,这不是一篇”21天精通Common Lisp”。你需要反复阅读这里或者别的地方提供的资料,直到你觉得自己”对Common Lisp有经验”了。(我不需要强调,掌握任何严肃的语言所需要的时间都远不止21天。)

1.2 一般性介绍

Lisp 拥有非常久远的历史。它最初被发明于1950年代,并在之后的岁月里不断发展进化。在不同的阶段中,Lisp产生了各种变体,所以Lisp实际上并不是某 一”个”语言,而是某一”种” 语言的总称。(把C, C, Objective C, Java以及C#都看作某一种”类C语言”的话,Lisp的情形也差不多。)

1980年代到1990年代之间,两种主要的变体被广泛 接受与使用,从而逐渐占据了主导地位: Common Lisp和Scheme。Scheme是1970年代,由Gerald Jay Sussman和Guy L. Steele发明的。他们发明Scheme的动机是试图理解面向对象,并将其引入Lisp。Scheme在概念层面上引入了lexical closures (作为对象的一种抽象)和continuations(作为对函数调用与消息传递的抽象);在技术层面上引入了对tail calls和tail recursions的”极度”优化。Scheme的优点在于,它像一颗美丽的宝石,能够吸引那些寻找美与真理(例如,最小化和正交性)的学院派。然 而,它缺少许多你在日常编程中需要的实用的东西,这些东西如果引入则会破坏概念上的完美。

如果你对以上提到的术语感兴趣,参见一下链接:

1. Lexical scope, dynamic scope, (lexical) closures: http://c2.com/cgi/wiki?ScopeAndClosures
2.Continuations: http://c2.com/cgi/wiki?CallWithCurrentContinuation
3.Tail calls, tail recursion: http://c2.com/cgi/wiki?TailCallOptimization

在1970 年代末的时候,若干种Lisp变种都在普遍使用。Common Lisp的产生是对当时分裂混乱状况的一种补救-综合当时流行的若干种Lisp的优点,避免它们的缺陷,形成一种统一的Lisp。进一步说, Common Lisp往前进了一大步,因为它采纳了从Scheme中得到的经验和教训。(Lexical Scope进入了Common Lisp。 Continuations因为实际使用太复杂而未被包括。Tail recursions的优化则被认为是一项自然纯粹的实现技术,没有必要被标准化)。

这些信息提示你,如果你今天考虑准备使用Lisp, 你应该使用什么?如果你希望作一些实际的”现实生活”中的事,你应当选择Common Lisp。如果你主要做学术研究且,比如说,希望尝试contiuations,你可以选择Scheme。两种语言现在都依然被广泛使用,你能从它们的社 区中得到支持。(此外还有一些Lisp变种存在,例如Emacs Lisp和Lush,但是它们都是针对特定领域的,能力比较有限)。

好几个理由促使我决定使用Common Lisp。其中的一些理由会在这份介绍中闪现。我不喜欢Scheme是因为它是一种”正确”的语言,非常强调事情应当如何完成。Common Lisp比起来比较自由。

然而,许多关于Scheme的书或者描述,对于Common Lisp也是有意义的。比如,描述什么是lexical closures。所以下面的章节中,偶尔也会提到一些关于Scheme的参考材料。

(我 的本意是:你选择Common Lisp或者Scheme无关紧要,这仅仅取决于你的特定的需要。让你的直觉来决定你该使用哪一种好了。事实上,两种语言都足够灵活,能够胜任任何研究或 者”现实生活”的工作。只是,我这里的介绍是针对Common Lisp的,如果你决定选用Scheme,你需要去别的地方找介绍材料。下面的”链接”一节中有一些这方面的指南。如果你觉得不喜欢Scheme或者 是Common Lisp的时候,别忘了尝试一下另一个。)

顺带说明一下人们是怎么使用Lisp和Scheme这两个名字的。广义来说, Lisp一般指称整个语言家族,包括Common Lisp, Scheme,更古老的变体(已经不再流行)比如MacLisp,InterLisp,Franz Lisp,以及更新的比如Arc和Goo。狭义来说,Lisp现在只指Common Lisp,不包含Scheme!到底是使用Lisp广义的意思,还是狭义的意思,取决于什么人在什么语境下使用。甚至对广义的理解也是不同的:有一拨人的 看法是不包括Scheme的,而另一拨人甚至包含了 Python和Dylan。(我的个人看法,如果在特定场合下它很重要的话,那就不要采用这样模糊的说法。)

关于如何阅读这份材料:我已经努力地让这份材料可以流畅得顺序读下来了。当然,你可以选择略过某一些章节,或者跳跃着读。后面的一些章节要求你至少能够了解一些Lisp的代码片断,为此文章中提供了足够了指南。

1.3 Common Lisp总纲

http://www.lisp.org/table/objects.htm 是一篇仅有一页的材料,却非常好的回顾了Common Lisp提供的特性。这篇描述着力于CLOS的面向对象特性(Common Lisp Object System是Common Lisp的一部分),同时也对Common Lisp的函数性属性与命令性属性做了不错的总结。

http://www.lispworks.com/products/lisp-overview.html 是Xanalys公司写的The Common Lisp Language Overview。

1.4 最初的障碍

Lisp的历史和其他语言,特别是Algol和C这一系的语言,不同。因此,Lisp社区在各个层次上采用的表示法和术语都和其他语言有所不同。这里先要澄清一些容易出现混乱的地方。

1.有一些Lisp的内建函数取的名字看起来非常可笑。比如”car”被用来得到列表的第一个元素,”cdr”被用来得到列表中剩余的元素。列表 使 用”cons”来构造。”mapcar”被用来遍历列表,等等。特别的, Algol或者Wirth家族语言的使用者会奇怪,为什么Lisp的设计者不采用更”自然”的名字,例如”first”,”rest”, ”foreach”等等呢?至少,这就是我面对以上那些名字时的第一反应(显然,我也不是唯一一个)。

请尝试忘记你以前关于”自然”的概念-主要是历史原因,使得Lisp有完全不同的文化。就好像,你可以宣称英语比法语更加”自然”,但这只是因 为你本人只会说英语。实际上,习惯这样一套完全不同的标记法只需要一到两天的Lisp编程经历,那以后这些问题就不存在了。Common Lisp也提供了一些可选的名字,比如”first”和”rest” 用来替换”car”和”cdr”,你也不是没有选择。不过你依然会常常读到那些使用传统名称的代码。

2.看起来Lisp使用了太多的括号。只要选择一个对Lisp的缩进风格支持良好的的编辑器,你很快就可以忘掉括号了。你几乎不用担心在Lisp中写错格 式。请记住,因为格式,如果你忘记了括号或者begin/end这样的关键字,如果你用错了控制指令(例如if,while,switch/case,等 等),C风格的语言和Pascal风格的语言一样会出错。然而,如果你是一个有经验的程序员,我猜你从来没遇到过这样的问题。使用Lisp也是如此。只是 一个适应的问题。

3.有些人依然认为Lisp只提供了列表这样一种数据结构。这种观点来自于他们对某种非常古老的Lisp变体的了解,或者是因为Lisp这个名字本身就是 ”List Processing”的缩写。事实上,Common Lisp提供了所有其他语言中能找到的有用的东西,比如structs(和C风格的structs很像),数组,真正的多维数组,对象(和 Smalltalk和Java中的那样,有成员数据和成员方法),字符串,等等。

类似的,有些人依然认为Lisp是一种解释型语言。事实上,现在已经很少有哪一种Lisp实现是纯粹的解释执行系统了。所有正经的实现都包括了编译器, 能够直接产生机器代码。所以,Lisp一般区分编译器期和运行期。这种区别在某些情况下很重要,比如做宏编程的时候。

但是这依然和所谓的”编译语言”,例如C和大部分Pascal变体,不同。大部分时间里,你不需要显式编译一个Lisp程序然后执行它。你只是和一 个Lisp开发环境进行交互,开发环境会自动编译Lisp代码。所以Lisp和那一些just-in-time语言很类似。Lisp 和某些语言一样有着交互的本质,比如Smalltalk(从某种意义上说,我们可以将Smalltalk归为某一种Lisp变体)。

最重要的一点是,Lisp是非常高效的语言。Common Lisp厂商都努力来提高非常好的性能。(请注意,很多现代的 Java IDE,比如JBuilder, NetBeans, Eclipse等等,都在向Lisp和Smalltalk那样的交互环境发展-所以,这种交互属性即使在Algol风格和C风格的环境下也得到认可。)

近期有一篇比较Lisp和Java与C++性能方面的比较:Erann Gat, Lisp as an Alternative to Java,位于http://www.flownet.com/gat/papers/。(如果你对该论文中提到的Java程序员的编程经验有疑问,请看http://www.flownet.com/gat/papers/ljfaq.html)。

4.描述Lisp时常用到”form”这个术语。Form是Lisp语言格式的基础。例如:

(format t “Hello, World”)

这个form在控制台打印”Hello, World”。(参数”t”在Common Lisp中表示标准的布尔值”truth”,对于format 函数来说,指待标准输出。)所以术语form相当于其他语言中的statements(语句)或者expressions(表达式)。一个Lisp 纯粹主义者会声称Lisp中不存在statements而只有expressions,不过这个对实践没什么差别。其他编程语言中, statements和expressions的差别,也因为存在所谓”expression statements”而变得模糊,这种”expression statements” 的返回值被直接忽略了。在Lisp中你也可以这样做,Lisp甚至定义了没有值的表达式(或者被定义为”未定义值”)。所以实践中,Lisp和其他语 言一样,一样地区别两者,又一样的模糊两者的差异。

Lisp中也存在所谓的”special forms”。要理解这一术语,你首先要理解Lisp中,函数怎么接受作为参数的 form。上面的例子中,函数”format”接受的参数是”t”和”Hello, World”。以下的例子中,函数”foo” 的参数是”bar”和”goo”。

(foo bar goo)

然而Lisp中也有一些form是不作为函数调用处理的。它们或者是宏,或者是”special forms”。”Special forms” 指的是有限的一套内建操作。(他们被Common Lisp环境”特殊”处理了。但是要注意,内建函数和宏都不是”special forms”)。一般都可以直接从form的第一个元素来判断,是一个函数form,一个宏form还是一个special form。这些不同form之间的区别有一些理论和实践上的意义,不过你现在用不着担心他们。当这样的区别变得重要的时候,你会注意到他们,而且很容易处 理。

5.Lisp社区中曾经有过,实际上现在依然在继续着关于”小”语言还是”大”语言的讨论。对于其他社区的人来说着听起来很让人困惑(至少是对我)。到底问题是什么?

Lisp的优点之一就是它不区分内建函数和用户定义的函数。例如:

(car mylist)

(cure mylist)

函数car是Lisp预定义的;而cure不是,所以cure应当是一个用户定义的函数。重点在于你无法从形式上区别他们之间的不同。和下面这个Java的例子做一下比较:

synchronized (lock) {
beginTransaction(…);
…
endTransaction(…);
}

Java中的synchronized语句是一个内建的特性,允许你将一个代码块保护起来,是对特定对象的访问序列化。这个例子中的事务处理函数也定义 了一个代码块,达到相同的事务语义,但你却一眼就能看出来它不是Java内建的特性。(是的,Lisp提供了面向块的语句-哦,不,是forms;而且, 你可以定义自己的面向块的form,并且保持和Lisp内建特性的相似性。)

现在,为了理解所谓小语言大语言的问题,你需要记住上述差异。

这个问题对于Java或者类似的语言的标准化来说,意味着什么?你首先必须标准化语言本身,然后你至少必须标准化一些函数库。就像前面提到的那样,内建特性和函数库之间有很明显的差别,所以这是两件不同的任务。

但是在Lisp世界中不是这样的。因为内建特性和函数库之间没有显著的差别,标准化函数库的过程是标准化语言的一部分(或者从某种程度上来说,反过来)。

所以所谓的小语言和大语言之争,归根到底就是小规模函数库和大规模函数库之间的区别。

总结起来,Scheme是一种小语言,因为它只提供了非常小的一套标准库;而Common Lisp 是一种大语言,因为它提供了很多有用且被标准化了的特性。然而,如果我们只关注语言最核心的部分,那么它们的大小实际上还是相当的。(进一步说,如果你查 看一个”严肃”的Scheme实现,你会发现它也提供了很多没有被标准化的函数库。)

虽然有概念上的差别,不过这对上述小大之争并无帮助。在Scheme和Common Lisp之间选择也不是仅仅因为语言的大小。

6.你也许听说过Scheme是”Lisp-1”的语言,而Common Lisp是”Lisp-2”的语言。另外一种说法是Scheme是one-cell的系统,而Common Lisp是two-cell的系统。人们谈论的是这样一个事实:变量的值和函数定义这两个方面,在Scheme是相同对待的,而Common Lisp中则不是。这并非出于实践上的理由,显然你用这两种语言都可以写出真正的程序。两种语言都提供了相应的方法,帮助解决作为一个”Lisp- 1”(或者”Lisp-2”)系统会面对的问题。你可以忽略这些细节,知道你真正遇到它们;而当你遇到它们的时候,你很容易发现如何正确处理。 ”Lisp-1”系统使得贯彻函数式编程的风格更加容易,而”Lisp-2”系统则使得宏编程更不容易出错。确切的细节有一点麻烦,如果你真的想 了解技术上的影响,你可以阅读以下论文。
1.Richard P. Gabriel and Kent M. Pitman, Technical Issues of Separation in Function Cells and Value Cells http://www.dreamsongs.com/Separation.html

1.5 Lisp的历史

有两篇出色的论文,提供了关于Lisp整个历史非常详细的信息,包括Common Lisp和Scheme,这是一件了不起的事情。某种角度看来,阅读这样两篇论文就像在读侦探小说,吐血推荐!

1.John McCarthy: History of Lisp (ACM SIGPLAN History of Programming Languages Conference, 1978) http://www-formal.stanford.edu/jmc/history/lisp.html
2.Guy L. Steele Jr. and Richard P. Gabriel: The Evolution of Lisp (ACM Conference on the History of Programming Languages II, 1992) http://www.dreamsongs.com/Essays.html

后一篇论文同时也提供了关于Lisp的非常全面的引用线索。

(如果读到Herbert Stoyan关于Lisp的论文,请不要被他搞糊涂了。我个人觉得他的文章很难读,而且推导出一些奇怪的结论。)

1.6 Lisp的技术背景

就 像我之前所说的那样,Lisp通过统一处理数据和代码,形成了一套完整的计算理论--这也是让Lisp如此强大的原因。我这里推荐两篇论文,它们出色的解 释了这种计算理论到底是什么。(它们涉及到”meta-circular interpreters”的概念--不过不要被术语吓倒,并不象听起来的那么难。)在我看来,你至少需要读第一篇,来真正了解Lisp的能力。

1.Paul Graham, The Roots of Lisp http://www.paulgraham.com/rootsoflisp.html

顺便说一句,这篇文章也很好的描述了Lisp中的基础(primitive)forms。

如 果你喜欢”The Roots of Lisp”,并且想知道更多关于”meta-circular interpreter”的情况,你可以阅读这一篇更深层次的讨论。你也会了解到lexical closures,dynamic binding等等的强大之处。

1.Guy L. Steele Jr. and Gerald Jay Sussman: The Art of the Interpreter or, the Modularity Complex (Parts Zero, One and Two), http://library.readscheme.org/page1.html

(这篇论文看起来像是会有续集的样子,但是实际上没有。别费劲去找了,尝试着把结尾作为一个练习吧。)

1.7 可用的实现

你可以在http://alu.cliki.net/Implementation上 找到一个 Common Lisp实现的列表。我只使用过其中的一小部分。许多Lisp程序员喜欢使用emacs或者xemacs作为它们的开发环境,不过我倾向于使用更现代化的 IDE,这很大程度上限制了我的尝试范围。(显然,这只是偏好的问题。如果你对将Emacs设置为一个Lisp IDE感兴趣,http://cl-cookbook.sourceforge.net/windows.html非常不错。)

向Apple Machintosh(Mac OS X)的用户特别推荐:LispWorks for Machintosh是一个出色的IDE,个人版可以免费下载。 Macintosh Common Lisp也是一个免费的IDE,但是和Mac OS X环境的集成不如LispWorks做的好。Digitool可以让你免费使用MCL 30天。CLISP, ECL, OpenMCL以及SBCL在Mac OS X下都可以使用,但是没有IDE。Franz Inc准备为Allegro Common Lisp在Mac OS X上推出一个 IDE,但是还没有具体计划。

我一般使用LispWorks for Machitosh和Machintosh Common Lisp。但是,这不是产品广告-请根据你自己的情况作出选择!

2 精选自学指南

这部分中,我会给一些建议和参考,以帮助你了解Common Lisp的若干方面。(主要是关于标准库的。)

2.1 参考材料

日常编程中,你一般需要一些参考材料,来查询函数定义,语法格式,等等。有的人喜欢看书,有的人则喜欢在线材料。

现在的Common Lisp标准,是1995正式作为ANSI标准发布的,是Common Lisp的权威说明。但是,因为它只给了标准而没有提供原理说明,所以它的可读性比较差。

在1990 年代初的时候,Guy L. Steele写了第二版”Common Lisp the Language”,来反应当时Common Lisp标准化的状况,那是已经非常接近最终标准了。(出版于1980年代的第一版,一般被称作CLtL,被CLtL2取代以后,没有再使用的必要。)虽 然CLtL2还是缺少一些ANSI Common Lisp中定义的特性,而且还有些特性和ANSI标准有出入,它还是被广泛推荐,作为入门材料,因为它的优势在于有非常详细的描述,而不仅仅是规范而已。 这帮助你理解材料。不过你手边还是需要一份ANSI标准,以便查得某一个定义的确切细节。进一步,ANSI文档包含了非常有用的术语表。

幸运的是,ANSI Common Lisp和CLtL2都有在线的HTML和PDF/PS格式可以下载。这里是链接:

1.ANSI Common Lisp: http://www.lispworks.com/reference/HyperSpec/

“HyperSpec”的编辑强调,这不是权威的规范,而仅仅是从标准ANSI规范的衍生物。然而,我猜测这仅仅是出于法律的原因。 HyperSpec是可以信赖的,实际上,两份文档是从同一份TeX生成的。

2.Franz, Inc.提供了一份不同风格的在线文档,http://www.franz.com/support/documentation/6.2/ansicl/ansicl.htm

3.CLtL2: http://www-2.cs.cmu.edu/afs/cs.cmu.edu/project/ai-repository/ai/html/cltl/cltl2.html或者:http://oopweb.com/LISP/Documents/cltl/VolumeFrames.html上有一个带Frame的版本。

注意:CLtL2的附录中有关于所谓的”Series”宏和”Generators and Gatherers”,可以实现lazy iterators。这些特性并未进入ANSI Common Lisp。我没有找到相关的理由,也无从了解为什么它出现在CLtL2中,却最终没有被收入进ANSI Common Lisp,我猜测是因为它们实验性太强。”Series”可以在http://series.sourceforge.net上找到。

4.一个关于CLtL2与ANSI Common Lisp之间差别的列表,位于http://home.comcast.net/%7ebc19191/cltl2-ansi.htm。

如果你更喜欢印刷品的化,你得花费一些时间来获得。CLtL2看起来已经绝版了,但是你还是可以尝试着从Ebay上得到一本旧的。在Amazon这样的在线书市上也可以找到。你还可以尝试一下出版商的主页:http://www.bh.com/digitalpress。

ANSI文档可以在http://global.ihs.com/上获得。不过我估计它既不方便也不有用,因为这本书超过1000页。而且价格将近$400,实在是太贵了。

Paul Graham的”ANSI Common Lisp”是一本很好的书,不过我觉得拿它做参考并不合适。我一般还是倾向于把标准规范打印出来(例如Java语言规范),不过对于Common Lisp来说,电子版的就可以了。

2.2 基础中的基础

Paul Graham写了一本非常不错的关于ANSI Common Lisp的书,叫做”ANSI Common Lisp”。非常适合阅读,而且幸运的是,作者在他的主页上公开了最初两章。我特别推荐第二章,它让你很快就能对Common Lisp编程有一个概念。

1. Paul Graham, ANSI Common Lisp, http://www.paulgraham.com/acl.html

如果你喜欢他的风格,你可以考虑购买。

那之后你需要的,只是查看CLtL2或者ANSI标准而已。下面是一些我个人认为比较难得到的信息。

为了能够彻底理解以下的章节,你需要先阅读”The Roots of Lisp”(http://www.paulgraham.com/rootsoflisp.html)或者是”ANSI Common Lisp Chapter 2”(http://www.paulgraham.com/lib/paulgraham/acl2.txt)中的一篇,或者你至少已经能够理解Lisp的代码。

2.3 基础的高级内容

以下是一些你早晚会碰到的小问题。

1.Common Lisp允许在函数定义中包含可选参数(optional arguments),剩余参数(rest arguments),关键字参数(keyword arguments),参数缺省值(arguments with default values)以及以上各种形式的组合。如何使用这些形式的确切规范可以在CLtL2的5.2.2(“Lambda Expressions”)和ANSI specs 3.4(“Ordinary Lambda Lists”)中找到。

(Lambda Expressions指未命名的函数。命名函数类似于其他编程语言中的过程(procedures),函数(functions)以及方法 (methods)。未命名函数类似于Smalltalk中的blocks,从某种意义上说,Java中的anonymous inner classes。参见CLtL2 5.2(“Functions”)以及相关章节中的说明。)

2. Common Lisp中,标准输出一般使用’format’函数。这某种程度上类似于C语言中的printf(不过,我承认我不是printf的专家!)。我还没有在 任何文章中见到过关于’format’不错的论述,所以你不得不去研读CLtL2中的相关章节。

CLtL2中,format是在22.3.3(“Formatted Ourput to Character Streams”)中描述的。对应的ANSI specs是22.3(“Formatted Output”)。

3.Common Lisp中的字符串有一点点复杂。首先是因为Common Lisp标准化在Unicode出现之前,它已经意识到ASCII或者其他一些有限的字符集是不够的。所以Common Lisp标准制定了处理字符串的一般方法,而把一些问题留给了具体的实现。

一般来说,这个问题只有在你需要和其他编程语言开发的程序进行交互的时候,才会凸现出来。Allegro Common Lisp,CLISP以及LispWorks都提供了 Unicode支持,其他的实现就我所知还没有。(当然,你可以自己实现对Unicode的支持,Lisp处理起这些事情来是足够灵活的。;-)

如果你想知道关于字符串的更多细节,参考CLtL2,ANSI specs以及你使用的Common Lisp实现自带的文档。

4.对于文件名字的处理,各个平台间是不同的。Common Lisp标准化了一个框架,以使得实现者可以根据具体操作系统的要求来填补框架中的留白。所以,又一次,你需要参考CLtL2,ANSI specs,以及你使用的Common Lisp实现自带的文档。

2.4 宏

Common Lisp的宏特性是这个语言的亮点之一-它是一种强大的方法,使得你可以书写自己的编程语言结构,而不仅仅是更多的函数而已。

宏一般被认为是高级主题,因为它非常强大,且看起来很难掌握。然而,我发现宏的概念还是非常容易理解的,而写起来也比较容易的。

你可以在Paul Graham的”On Lisp”中找到关于宏的非常好的描述,以及关于如何使用宏的说明。这本书已经绝版了,但是你可以免费从以下URL下载到电子版

Paul Graham, On Lisp, http://www.paulgraham.com/onlisp.html

以下是我认为的一些注意事项:

1.Common Lisp中的宏和C中的宏并不相同!就我所知,C语言中的宏只是对字符序列的处理,并不关心所处理的程序结构。相反的,Common Lisp中的宏直接作用于程序结构。这完全是因为Lisp中将数据和代码一致处理,既然程序就是一个forms构成的列表,那么宏就可以像处理其他列表一 样地处理forms的列表。

所以你看到术语”宏”的时候,请三思 – 特别是对于其他一些尚在发展中的编程语言。

(因为我不会再讨论C语言中的宏,从现在开始,我提到”宏”的时候,都是指Lisp的宏。)

2.宏可以被理解为一种接受参数,产生新代码的函数。在编译过程中,每发现一个宏,都会把相应的form传递给宏,然后用得到的结果替换最初的form。然后编译过程继续处理新得到的form。这个过程被称为”宏展开”。

下面这个例子定义”cure”为”car”。

(defmacro cure (argument)
(list ‘car argument))

所以,每当你看到(cure a),其结果都是编译期的(car a) – 宏的结果表示在使用宏时 真正用到的代码。

(请注意这个例子只是用于展示概念。实际上它不是一个使用宏的好例子: “cure”最好是被定义为一个函数。)

你所看到的事实是,宏是Turing-complete的。所有Common Lisp的能力在编译期都可以使用,来帮助宏产生新的代码。宏可以包含其他的宏展开和函数,甚至包括迭代、递归,condition,等等等等。而且结果 中可以进一步包含其他的宏,这使得”宏展开”不断重复,直到最终结果中只包含纯粹的函数。

举例来说,Common Lisp中的宏可以用来做和C++中template meta-programming一样的事情。特别的,它可以用来进行Generative Programming!(Generative Programming的基础是定义一种 domain-specific的语言,开发相应的编译器来将语言转化为general-purpose的编程语言。)

3. 一般介绍到宏的时候,都会提到”backquote”这种形式。我认为这种介绍方式使得事情无端变得复杂起来。事实上,当你理解前述的关于宏的 Turing-completeness特性时,你已经在概念层面上对宏有了全局认识。然而,宏和backquote的组合在实践中还是很有益处的。以下 我会介绍backquote的格式,以及它如何和宏进行配合。

如前所述,Lisp中的大部分form都是函数。所以(car mylist)从mylist对应的列表中得到第一个元素。但有时候,你不希望对form进行求值(你不希望将这个form当作函数来处理)。比如,在 (car (a b c))中, (a b c)缺省不被认为是一个三元素组成的列表,而是函数a以及参数b与c。如果你想避免对(a b c)求值,你必须使用一个特别的 form,”quote”:(car (quote (a b c)))。现在,整个表达式的求值结果为”a”(列表(a b c)中的第一个元素。) 注意(quote …)不是一个函数,而是一个特殊的form(Lisp的内建属性)。

因为quote在Lisp使用是如此的频繁,我们发明它的简化形式: (car ‘(a b c))就可以表示(car (quote (a b c)))了。顺便说一句,quote也可以用来避免对一个变量求值。变量在Lisp中只是名字而已(和在其他编程语言中一样),所以如果你写下,比如说, (car a),得到的将会是变量a所引用的列表的第一个值。为了避免对变量求值,你可以写作(quote a),或者’a,你可能已经猜到了。

有时候,你需要对一些表达式或者变量求值,然后将结果组合成一个列表。Lisp中最简单的方式是: (list a (car b) c),先后对a, (car b)以及c进行求值,并把各个结果拼接成一个列表。又有时候,你只需要对其中部分进行求值。这也难不到你:(list a (car b) ‘c),先后对 a和(car b)进行求值,这样就可以保证得到的结果中,列表的最后一个元素肯定是c。

有时候,需要quoted(不求值)的表达式数量上要远远多余需要求值的表达式。这时,backquote就可以帮上忙了。它逆转了缺省的行为,使得缺 省的所有表达式都不被求值,需要被求值反而需要标记。以下例子中`(a b c ,(car d)) 和(list ‘a ‘b ‘c (car d))完全一样,符号a,b以及c都没有被求值,而(car d)被求值了。在backquote 形式下,需要被求值的表达式需要用逗号标出。(注意,这和quote形式不同,在quote形式中,整个表达式都会被作为quoted处理,且不能使用逗 号来对其中的一部分进行求值。)

所以,本质上,backquote形式只是一个简化,便于程序员临时改变求值与否的缺省行为。

现在,我们回忆一下之前的例子。

(defmacro cure (argument) (list ‘car argument))

从之前的讨论中,你可以推断出这和一下形式是等价的。

(defmacro cure (argument) `(car ,argument))

你现在已经可以理解,backquote形式对于宏来说为什么那么重要:宏定义本身和最终要产生的代码形式上更加接近了。(还记得嘛?(cure mylist) 在编译期会变成(car mylist)。)

顺便说一下,这里有一篇关于backquote形式历史的论文,非常不错。

Alan Bawden, Quasiquotation in Lisp,http://citeseer.nj.nec.com/bawden99quasiquotation.html

4.宏也常常成为Common Lisp和Scheme两派人马之间论战的焦点。Common Lisp中的宏有可能意外的捕获宏定义周围代码中的变量名。这听起来很危险,但是实际上不是。请看Paul Graham的”On Lisp”一书中对这个问题的详细讨论 – 我在这里不准备讨论细节。

Scheme提供了一种所谓”hygienic macros”的概念(简单的说,”hygiene”),来避免捕获变量。我所看到的问题是,hygienic macros被简单的描述为对macros的发展。常见的说法是: “Common Lisp的宏可能捕获变量名; hygiene避免了这一点,所以程序更安全了。”这种论点有一些正确,因为它在某些角度上延续lexical closures的概念。

然而这并不是真相的全部。有时候,你确实需要捕获变量名,而hygiene使得这件美好的事情变得无谓的复杂。更进一步说,在Common Lisp中避免变量名捕获实际上也是非常容易的,Paul Graham在他的书中展示了这一点,所以解决它并没有实际价值。

我的观点是,hygiene在Common Lisp中不是必须的(实际上也没有提供) ,因为写出安全的宏实际上很容易。除非你在理论层次上对这个问题感兴趣,你完全可以忽略hygienic macros这个概念。

(在Scheme中,情况略有不同,因为Scheme将变量和函数定义放在同一个名字空间里--所谓的Lisp-1。这意味着意外捕获函数定义的情况在 Scheme中发生的可能性要远大于在Common Lisp中。因此,Scheme更迫切地要解决这个问题。有一篇非常好的文章描述了macro hygiene问题:
Alan Bawden and Jonathan Rees, Syntactic Closures,http://citeseer.nj.nec.com/bawden88syntactic.html

文章给出的例子,可以作为一个非常好的练习:为什么这些问题在”Lisp-2”中并不突出?在什么样的情况下你会需要捕获变量名。

也可以参看上面提到的Gabriel和Pitman所写的关于隔离function和value名字空间的论文。 )


本人新博客地址