Lisp语言诞生的时候就包含了9种新思想。其中一些我们今天已经习以为常,另一些则刚刚在其他高级语言中出现,至今还有2种是 Lisp 独有的。按照被大众接受的程度,这9种思想依次如下排列。
(1) 条件结构(即 if-then-else 结构)。现在大家都觉得这是理所当然的,但是 Fortran I 就没有这个结构,它只有基于底层机器指令的 goto 结构。
(2) 函数也是一种数据类型。在 Lisp 语言中,函数与整数或字符串一样,也属于数据类型的一种。它有自己的字面表示形式(literal representation),能够存储在变量中,也能当作参数传递。一种数据类型应该有的功能,它都有。
(3) 递归。Lisp 是第一种支持递归函数的高级语言。
(4) 变量的动态类型。在 Lisp 语言中,所有变量实际上都是指针,所指向的值有类型之分,而变量本身没有。复制变量就相当于复制指针,而不是复制它们指向的数据。
(6) 程序由表达式组成。Lisp 程序是一些表达式树的集合,每个表达式都返回一个值。这与 Fortran 和大多数后来的语言都截然不同,它们的程序由表达式和语句组成。
区分表达式和语句在 Fortran I 中是很自然的,因为它不支持语句嵌套。所以,如果你需要用数学式子计算一个值,那就只有用表达式返回这个值,没有其他语法结构可用,否则就无法处理这个值。
后来,新的编程语言支持块结构,这种限制当然也就不存在了。但是为时已晚,表达式和语句的区分已经根深蒂固。它从 Fortran 扩散到 Algol 语言,接着又扩散到它们两者的后继语言。
(7) 符号类型。符号实际上是一种指针,指向存储在散列表中的字符串。所以,比较两个符号是否相等,只要看它们的指针是否一样就行了,不用逐个字符地比较。
(9) 无论什么时候,整个语言都是可用的。Lisp 并不真正区分读取期、编译期和运行期。你可以在读取期编译或运行代码,也可以在编译期读取或运行代码,还可以在运行期读取或者编译代码。
在读取期运行代码,使得用户可以重新调整(reprogram)Lisp 的语法;在编译期运行代码,则是 Lisp 宏的工作基础;在运行期编译代码,使得 Lisp 可以在 Emacs 这样的程序中充当扩展语言(extension language);在运行期读取代码,使得程序之间可以用S表达式(S-expression)通信,近来 XML 格式的出现使得这个概念被重新“发明”出来了。
Lisp 语言刚出现的时候,这些思想与其他编程语言大相径庭,后者的设计思想主要由50年代后期的硬件决定。随着时间流逝,流行的编程语言不断更新换代,语言设计思想逐渐向 Lisp 靠拢。思想(1)到思想(5)已经被广泛接受,思想(6)开始在主流编程语言中出现,思想(7)在 Python 语言中有所实现,不过似乎没有专用的语法。
思想(8)可能是最有意思的一点。它与思想(9)只是由于偶然原因才成为 Lisp 语言的一部分,因为它们不属于麦卡锡的原始构想,是由拉塞尔自行添加的。它们从此使得 Lisp 语言看上去很古怪,但也成为了这种语言最独一无二的特点。说 Lisp 语言古怪倒不是因为它的语法很古怪,而是因为它根本没有语法,程序直接以解析树(parse tree)的形式表达出来。在其他语言中,这种形式只是经过解析在后台产生,但是 Lisp 直接采用它作为表达形式。它由列表构成,而列表则是 Lisp 的基本数据结构。
用一门语言自己的数据结构来表达该语言是非常强大的功能。思想(8)和思想(9),意味着你可以写出一种能够自己编程的程序。这可能听起来很怪异,但是对于 Lisp 语言却是再普通不过。最常用的做法就是使用宏。
术语“宏”在 Lisp 语言中的意思与其他语言中的不一样。Lisp 宏无所不包,它既可能是某样表达式的缩略形式,也可能是一种新语言的编译器。无论是想真正理解 Lisp 语言,还是只想拓宽编程视野,最好都学学宏。就我所知,宏(采用 Lisp 语言的定义)目前仍然是 Lisp 独有的。一个原因是为了使用宏,你大概不得不让你的语言看上去像 Lisp 一样古怪。另一个可能的原因是,如果你想为自己的语言添上这种终极武器,你从此就不能声称自己发明了新语言,只能说发明了一种 Lisp 的新方言。
我把这件事当作笑话说出来,但是事实就是如此。如果你创造了一种新语言,其中有 car、cdr、cons、quote、cond、atom、eq 这样的功能,还有一种把函数写成列表的表示方法,那么在它们的基础上完全可以推导出 Lisp 语言的所有其他部分。事实上,Lisp 语言就是这样定义的,麦卡锡把语言设计成这个样子就是为了让这种推导成为可能。