Skip to content
On this page

第七课:对子

最后我们来看 Lisp 最核心的东西——对子。简单来说,对子就是由两个东西组合在一起构成的东西,类似数学上的“有序数对”。在 Lisp 中,使用 cons 过程来构造一个对子。

scheme
> (cons 1 2)
'(1 . 2)

解释器输出了 '(1 . 2),就代表这是一个用 12 组合起来的对子。(感觉很像列表的输出格式?别急,往下看看就知道。)

对子是有序的。用 car 过程输出对子的左边的部分;用 cdr 过程输出对子的右边的部分。

scheme
> (define x (cons 1 2))
> (car x)
1
> (cdr x)
2

你可能大脑已经开始飞速旋转了——怎么这个和列表的那个操作都叫 car cdr 呢?函数重载吗?还是什么特殊的语法?……

好了不卖关子了,其实列表就是由一系列对子构成的。看这个例子:

scheme
> (cons 42 (cons 69 (cons 613 '())))
'(42 69 613)

当我连续用三个 cons 过程构建出三个对子后,解释器直接将它解释为一个列表。首先,第一个对子的左半部分是 42,右半部分是一个对子;这个对子的左半部分是 69,右半部分又是一个对子;最后这个对子的左半部分是 613,右半部分是空表。当一系列对子以“右半部分”链接起来且最后一个右半部分是空表的时候,那么它就是一个列表。列表中的元素,就是这些对子的左半部分的值。

因此,将 car 作用在列表上,输出的正是第一个元素。因为列表作为对子的左半部分,就是第一个元素。将 cdr 作用在列表上,输出的就是除首元素外的列表,因为第一个对子的 cdr 就是这样的一个列表。

TIP

这种由对子构建列表的方式非常像其它编程语言中常见的“链表”。

cons-cells

接下来的内容和使用 Lisp 编程已经没有太大关系了,但如果你想编写一个 Lisp 解释器,那么可以继续读下去。

对子是 Lisp 中唯一的复合数据类型。其余的数据类型,比如整数 42、有理数 5/2、浮点数 3.14、布尔值 #t,都是原子类型,即不可再分的。而基于对子,不仅可以构建出列表,还可以结合一些语言特性构建出更复杂的数据结构(比如树、散列表等等)。

列表不仅构成 Lisp 程序的大量数据结构,也是 Lisp 源码的结构。不论是特殊形式,还是组合式,它们不都写成列表的样子吗(少了一个单引号)?

scheme
(define x 42)  ; 看上去很像由 define、x 和 42 构成的列表
(+ x 1)        ; 看上去很像由 +、x 和 1 构成的列表

它们不仅是列表的样子,它们实际上就是列表。用源码 (define x 42) 举例;它其实是由符号 define、符号 x 和整数 42 构成的列表。

这里提到了“符号”。符号是其它编程语言中很少出现的概念。在 Lisp 中,你可以将符号直接理解为一种特殊的字符串类型——用作变量名的字符串,目前就先这样理解好了。使用一个单引号再加上变量名,就可以或得到表示这个变量名的符号:

scheme
> 'define
'define

如果将符号 '+、符号 'x 和整数 1 构成一个列表,那么你就得到了源码 (+ x 1) 的表示形式。把源码的表示形式塞到 eval 过程中,就可以直接对这个源码求值!

scheme
> (define x 42)
> (define source (list '+ 'x 1))
> (eval source)
43

从这个案例我们可以一窥 Lisp 解释器的构造。假设 Lisp 解释器正在解释一个列表形式的源码。如果列表的 car 是一个符号,而且符号名指代了特殊形式如 'define,那么执行相关的特殊逻辑。否则,对列表中的每一个元素进行求值

  • 对整数、布尔类型的求值结果仍然是它自身——这种元素类型称为自求值类型
  • 对符号类型的求值结果,就是查找当前环境定义的所有变量名,找到这个符号名对应的值作为求值结果。
  • 对列表类型求值,就是调用过程的逻辑了。将列表的 car 部分作为要调用的过程,将 cdr 部分作为实参,然后绑定到对应形参,执行过程的定义。

最后讲一个特殊形式 quotequote 特殊形式接受一个东西,不做任何求值操作直接原样返回。比如:

scheme
> (quote (+ 1 2))
'(+ 1 2)          ; 由符号 +、整数 1 和整数 2 构成的列表

quote 特殊形式还可以简写成 '。因此 '(1 2 3) 就是 (quote (1 2 3)) —— 解释器就解释为 '(1 2 3)。哈哈,绕了一圈又回到起点。这就能解释为什么解释器输出的列表长成 '( ... ) 的样子,因为你直接把这个样子作为源码输入到解释器,得到的就是这个东西。

TIP

思考题:请问 (car ''x) 求值得到的结果是什么?为什么?试一试,想一想。