Common Lisp学习笔记(1):语法和语义

按:本文是我在阅读Peter Seibel所著的Common Lisp教程Practical Common Lisp(免费在线阅读地址:http://gigamonkeys.com/book/)的学习笔记。本篇主要依据于原书的第四章Syntax and Semantics,主要的内容也如其题目所描述,是Common Lisp的语法和语义。

之前已经在这个博客上发表过《Common Lisp学习笔记(0):从SLIME开始》,不过后来由于课业比较忙,虽然还是能抽出些时间继续阅读这本书,但是一直没来得及写一些后续的笔记。而在这期间,我发现竟然有一位名叫田春的程序员翻译了这本书,并且在今年10月出版,于是我也购买了这本书。既然前面阅读英文原版得到的东西在现在已经记得不太牢靠,我打算凭借这本中译本来帮助我回忆这些内容,形成这些笔记,同时我依然会拿英文原版做对照。

注意虽然Common Lisp是Lisp的一种方言,二者并不相等,不过为了方便起见下文中我不会对Common Lisp和Lisp做太多刻意区分,大多数情况下以Lisp代指Common Lisp。

目录

S表达式

Lisp的语法和我们平时所接触到的诸如C、Java、Python之类的语言的语法颇为不同,Lisp的发明者John McCarthy称Lisp的这种语法为S表达式(S-expressions),它的一个最明显的特点就是往往会有大量的括号。这点我们可以从下面两幅 来自外刊IT评论的图片中看出来:


C++按键频率图
Common Lisp按键频率图
这两幅图分别是C++和Common Lisp两种编程语言的代码的按键频率统计,其中颜色越红的地方按键频率越高。可以看出Common Lisp的语法有多么特点鲜明。显然很多人会对Lisp中存在如此多的括号表示不解,而许多Lisp程序员会认为这正是Lisp最美好的东西。因为一对括 号在Lisp中就是一个链表(list),所以对于(+ 1 2)这种表达式,你既可以把它用作语句,也可以把它作为一种数据来进行一定的操作,后者也是Lisp的宏(macro)这又一个被许多Lisp程序员所推 崇的事物中常用到的一种技巧。


S表达式的基本元素是list和atom。其中list是指由括号包围的,其中可能包含许多由空格分隔的S表达式元素的东西,而atom是除此之外的东西 (这里的定义其实有些递归的味道,而递归在Lisp中将是很常用的)。从技术层面上来看注释不属于S表达式。这里我用list而不用中文翻译过来的词语, 是因为list既有链表也有列表的意思,在前文中我们已经用过“链表”这种表述,是为了描述其数据结构(不过值得注意的是,对于一个list的语 句,Lisp的解释器或编译器在实现的时候不一定是真的把它用链表这种数据结构来存储的);但是把一堆元素列在一个括号里,描述这一概念无疑用“列表”这 种词语更恰当一些。但是如果一会儿使用词语链表,一会儿又使用词语列表的话,是很容易混乱的,所以我在这里决定对list和atom这些元素都直接使用英 文原词。


我们先来看atom。常见的atom有数字、字符串和名字。


任何数字序列都都会被当作一个数字,这个数字序列可能以+或者-开头,包含小数点(.)和分数符号(/),或者由一个指数标记结尾。我们可以用多种方式来 表示同一个数字,但是无论你怎样书写,所有的有理数(整数和比值)在内部都会用它的“简化”形式来表示。你可以使用SLIME的REPL来验证这一点,无 论你输入的是4/2还是2/1,它们都会用2来表示,这也意味着4/2、2/1和2是相等的。但是值得注意的是2.0这类小数并不会被“化简”为2,因为 尽管2.0和2的值是相等的,但它们却代表了不同的对象。有过C之类的语言编程经历的读者可以拿int和float这样的类型差别来理解这一点。


字符串是由双引号(”)所包围的内容,里面往往是一串字符。在字符串中,反斜杠()是一种特殊的符号,它会转义接下来的任意字符。比如说如果想要在字符串 中添加双引号,可以使用类似于””and what is the use of a book,” thought Alice, “without pictures or conversation?””的表示方法(你可以在SLIME中使用(format t “blahblah”)的方法查看这个字符串的输出效果)。此外,如果想要在字符串中添加一个真的反斜杠,同样可以使用反斜杠来转义反斜杠,比 如”Within a string a backslash ($$ escapes the next character, causing it to be included in the string regardless of what it is.”注意Lisp的转义规则不同于我们比较熟悉的C这类语言的转义规则,如果在Lisp中使用类似”foon”这种表示方法,你得到的将 是”foon”而不是”foo”和一个换行符。这是因为Lisp的反斜杠转义符只是用来把一个字符转义为它这个字符本身的字符上的含义,比如用来让”和能 够以其字符含义被包含在字符串中(但是好像它在字符串里的用途好像也就这两样了),所以”n”其实只是让n以其字符含义n被包含在字符串中,和”n”并没 有什么本质差别。

原书这一章中并没有把字符作为一种常见的atom列出来,可能因为它在Lisp中并不像字符串那么常用,尽管后者其实是前者组成的一维数组。所以这 里我仅仅略微提一下字符的表示方法,为#后面跟上想要表示的字符,比如#X表示X,此外还可以用#Newline之类的方法表示换行符等,这些将根据原书 的内容在后面章节的笔记中提到。


最后一个常见的atom是名字。名字可能是变量名、函数名或者其他什么东西的名字。几乎任何字符都可以出现在一个名字里,不过要注意以下几点:

  1. 空白字符不可以出现在名字中,因为空白字符是用来分割list中的元素的。
  2. 数位(digits)可以出现在名字中,只要整个名字不会被解释为一个数字。
  3. 句号可以出现在名字中,但一个名字中不能只有句号。
  4. 开括号、闭括号、双引号、单引号、反引号、逗号、冒号、分号、反斜杠和竖线不能出现在名字中。(不过可以通过使用反斜杠转义这些符号,或者使用竖线包裹含有需要转义的名字,比如foo或者|foo|)

与C等很多语言不同的是,Lisp中的名字不区分大小写,在这些名字被读取后都会被统一转换为大写形式。即foo和Foo和FOO都对映同一个符号 FOO,不过foo和|foo|会被读取为foo,和FOO是不同的对象。近年来标准的编码方式是在代码中使用小写来写名字,由Lisp的读取器将其转化 为大写。

关于Lisp名字有一些约定,比如在Lisp程序中更倾向于使用hello-world这样风格的名字,而非hello_world或者 helloWorld;此外,全局变量名以*开头和结尾,常量名以+开头和结尾。有些程序员会在一些特别低层的函数名前加上%甚至%%。语言标准定义的名 字应是只使用字母(a-zA-Z)和*、+、-、/、1、2、<、=、>、&。


将atom们,或者list们,或者二者混合地放在一对括号当中,就成了一个list。通过list和atom就可以构造S表达式。比如x就是一个S表达 式,它表示一个名字为x的atom;()也是一个S表达式,它表示一个空的list;(1 2 3)表示含有1、2、3三个数字atom的list;稍微复杂一点的S表达式可以是这样的:

作为Lisp形式的S表达式

Lisp中的S表达式在Lisp中会从文本形式被转化为一种Lisp形式,然后作为Lisp形式被求值。最简单的Lisp形式当然是atom,这里 就省些功夫不赘述了。而对于list,Lisp中有三种类型的list形式,它们会以三种相当不同的方式求值。这三种形式为:函数调用形式 (function call form)、特殊形式(special form)和宏形式(macro form)。


函数调用形式的list中第一个元素为一个表示函数名的atom,后面是参数,其中每个参数也必须是Lisp形式。在对函数调用形式的list进行求值的 时候,会先计算list中除了第一个元素(即函数名)以外的所有元素,然后把求值结果作为参数传给list中第一个元素所代表的函数。比如(+ 1 2)会先对1和2进行求值(当然求值结果就是它们本身),然后把它们传给+函数,而像(* (+ 1 2) (- 3 4))这种会先计算(+ 1 2)和(- 3 4)的值(由于这两个元素也是函数调用形式的list,对这两个元素的计算也会按照先计算参数,再把它们交给函数名的步骤求值),然后把它们的值(3和 -1)传递给*函数。


特殊形式的list中第一个元素为特殊操作符(special operator)。这种形式的list是用来处理某些函数形式的list无法达成某些效果的情况的。比如如下形式的一个list:

我们希望能在x为真的时候输出yes,反之输出no。但如果IF是一个函数的话,Lisp就会先分别对x、(format t “yes”)、(format t “no”)三部分进行求值,然后把它们作为参数交给IF函数——这时候IF函数一起起不到什么作用了,因为两个format已经把yes和no都输出出来 了。所以IF注定不能是一个函数,而是Lisp的25个特殊操作符之一——每个特殊操作符都会按照它的特定规则就行求值。以IF操作符为例,它的基本形式 为:

其规则为,先对test-form进行求值,如果求值结果不是NIL,即对then-form进行求值,否则,对else-form进行求值(如果有else-form的话)或者返回NIL(如果没有else-form的话)。


宏形式的list中第一个元素为宏的名字。宏形式的list求值分为两个阶段:首先,宏形式的元素不经求值即被传递到 宏函数中;然后再由宏函数返回的形式——称为展开式(expansion)——按照正常的求值规则进行求值。注意对宏形式的list进行求值的这两个步骤 发生在不同的时间,生成展开式的工作发生在编译期,而运行期运行的是已经被展开的无宏的代码。这样子我们无需在运行期为宏形式付出额外的代价。我们可以从 下面的例子中看出其求值顺序。

首先是一个非常简单的宏定义:

注意这里(“hello world!” t format)并不是一个正确的list形式(它不属于我们所讨论的三种形式的任意一种),如果对直接其求值的话会被报错。但是宏形式的list实际上是 首先调用了REVERSE函数把这段代码逆转了过来(这里就是前面我们所说的把代码当作数据来操作的地方!),这时我们就得到了一个正常的函数形式的 list,这个list将作为BACKWARDS宏的展开式,按照函数形式list的求值规则进行求值计算。

Lisp中的真、假、等价

Lisp对真、假的定义也和其他语言不太一样。在Lisp中,NIL是唯一的假值,其他所有的都是真值。这种表示方法简单、直接,不过也是略有点疼 的,因为NIL在Lisp中不但可以用来表示假,还可以用来表示空列表(它是Lisp中唯一一个既可以表示atom也可以表示list的对象),所以其实 ()也是被当作NIL来读取的。值得注意的是,NIL是一个以符号NIL作为它的值的常值变量名,所以对它进行求值和直接把它的名字作为值结果其实是一样 的。还有一个变量T也是这样,它的值也是T。

Lisp还有一个比较蛋疼的地方是,它有多种进行等价比较的函数,而它们的名字绝对会让你抓狂。比如在这一章所讨论的有四个,它们分别是EQ、EQL、EQUAL和EQUALP。下面我列一张表来对它们进行比较:

函数名 说明 举例
EQ 判断“对象标示”(object identity)是否相等。只有当两个对象相同时才是等价的。但是数字和字符的对象标示取决于该平台上Lisp的实现方式,所以在不同平台上结果可能是不一样的。 在我的Fedora 15机器上,使用SBCL作为Lisp的实现,在SLIME中测试可以得到如下结果:

结果:T

结果:NIL

结果:T

结果:NIL
EQL 相同类型的两个对象表示相同的数字或字符值时,它们是等价的。
结果:T

结果:NIL

结果:T

结果:NIL
EQUAL 除了被EQL视为等价以外,递归来看具有相同结构和内容的list被视为等价(比如字符串)。
结果:T

结果:NIL

结果:T

结果:T

结果:T

结果:NIL
EQUALP 除了被EQUAL视为等价的以外,仅在大小写上有区别的字符、数值相同的数字,以及递归来看每个元素EQUALP等价的数据结构被视为等价。
结果:T

结果:T

结果:T

结果:T

结果:T

结果:T

注释

Lisp中以分号开头的行被视作注释行。通常注释根据其适用范围前置一个到四个分号,如原书中的这段代码所演示的: