Scheme中表(list)和序对(pair)的差别
很长时间没有更新博客了。我最终还是决定用中文来写我的博客。开始的时候,觉得用 英文写博客有很多好处。比如,英文比中文更加普及,这样就有更多人能看懂我的文字。 但是后来想了想,还是有问题:毕竟我写的东西给别人的帮助恐怕很小,而更多是为了 对自己的学习和生活的总结。在这种情况下,为了能够更好的表达内容(毕竟,中文是 我的母语),我还是决定使用中文。
其实,我发现,我还是喜欢中国的文字,哈哈。现在言归正传。
在学习 Scheme 的过程中,我发现pair和list这两个东西十分重要。list应该是 Lisp 中最为常用的数据结构了,而pair是所有数据结构的基础,list也是由pair搭建出 来的。然而,这两个东西有着一些共同点,让人在入门学习的时候难以很好的理解和区分。 至少我是这样的。网上也没有很详细的对这两个东西进行介绍的文章, SICP 的开始部 分我觉得也没有完全解释好这块。我便在本文尝试将这两者慢慢道来,或许能做些区分, 让我们更加深入的理解这两个东西。
什么是pair(序对)?
说是理解pair和list,其实最核心的当然还是pair。如果pair明白了,再需要知道的就 是怎么用pair构建list。顾名思义,序对(pair)便是一对数。这个翻译是我从 SICP 上边摘抄的。还有一个中文名字叫做”点对“,我想主要是因为 Lisp 中,对序对的表 示是用两个元素中间加一个”.“表示的。比如:
(cons 1 2)
返回的结果就是:
(1 . 2)
这就是一个最简单的点对了。有几个常用的函数用来操作点对:
| 函数名 | 功能 |
|---|---|
| (cons a b) | 创造点对(a . b) |
| (car p) | 返回点对p的第一个元素 |
| (cdr p) | 返回点对p的第二个元素 |
| (pair? p) | 判断p是否为一个点对 |
之前创造的点对p,用图来表示,就是:
|---|---| | 1 | 2 | |---|---|
什么是list(表)?
让我们先来从概念上知道list,再慢慢道来。其实,用过 Python 的一定不会对list陌 生,因为在 Python 中,到处都是list。在 Lisp 中,list的声明如下:
guile> (list 1 2) (1 2)
这样,就构造了一个长度为2的表,内容分别为数字1,2。用图来表示的话,就是:
|---|---| |---|----| | 1 | ------> | 2 | () | |---|---| |---|----|
如果我们和pair的点对p比较看看,我们会发现这个图已经很明确的表示出list和pair的 差别了。这里其实产生了两个点对q1和q2。q1的第一个元素是1,第二个元素是一个”指 针“,指向第二个点对q2;而q2的第一个元素是2,第二个元素是(),也就是”空表“,类 似于C中的NULL指针。而这个图也从最简单的例子描述了: pair是如何实现list结构的。
开始的时候,我觉得(1 2)这种表示非常奇怪,而我认为点对才应该这么表示。随着我 逐渐的学习 Scheme ,我大概明白一点了。我的理解是:
设计Scheme的人是数学家,他们喜欢抽象的东西,比如list,而list也的确是最常用的 结构。像(1 2)这么简洁的表示,还是给list这种最为常用的结构才好。
利用list,我们可以方便的构造一些复杂点的结构,如:
guile> (list (list 1 (list 2 3)) 4 5) ((1 (2 3)) 4 5)
|---|---| |---|---| |---|----|
| | | ------> | 4 | ------> | 5 | () |
|-|-|---| |---|---| |---|----|
|
\|/
|---|---| |---|----|
| 1 | ------> | | | () |
|---|---| |-|-|----|
|
|
\|/
|---|---| |---|----|
| 2 | ------> | 3 | () |
|---|---| |---|----|
上面这种绘图也是从 SICP 学的。每一行对应树的一个深度。我们会发现一个规律:
完全用list构造的结构,其中的所有的序对的cdr,也就是第二个元素,只有可能是指 向另一个序对的指针,或者空表()。
我本来想尝试实现一下list的构造函数,但是我发现list函数是一个参数长度不定的函 数,这就需要 Scheme 中对不定长函数定义的支持,而这种定义是需要用到list作为 剩余参数的容器的。所以作罢。
一个经典的list遍历:map实现
map函数的介绍
map是一个非常常用的函数,它可以对一个list中的每一个元素进行同样的操作,然后 将操作结果组成一个与原表同样结构的新表返回。它的定义往往是:
;; function: is a process that has only one argument ;; sequence: is a list to be processed by the function ;; return value: new list (define (map function sequence))
这个map不但可以用于list函数产生的表,对于任何用cons组成的数据结构都是可以的。 我在这里实现了一个map函数,为了和 Scheme 中的标准map函数区分开,我这里起名 字叫做map2。
map2的实现
这个过程中,lst参数虽然可以是一个list,但是操作完全是用pair完成的。这里,需要 我们对pair和list有很好的理解才可以。这也是我为什么将这个例子放到这里的原因。 这个例子也是参照 SICP 中的例子来的。
(define (map2 func lst)
(cond ((null? lst) '())
((not (pair? lst)) (func lst))
(else (cons (map2 func (car lst))
(map2 func (cdr lst))))))
在 SICP 的书第二章中,很多地方用到了这种判断逻辑,来遍历一个list:
- 用 null? 判断是否是空表,如果是,则返回空表;
- 用 not 和 pair? 判断是否不是 pair ,如果不是,则是一个元素;
- 否则,是一个 pair ,进而可以进行拆分处理,后再用 cons 进行组合。
这样的递归可以保留原始表的结构,而对每一个元素进行遍历。
注意的一点
如果使用过 Scheme 本身的map函数,我们会发现,我们实现的map2并不和map等价。 Scheme 中的map不能对点对进行操作,就算是list中包含点也是不行的。比如,如 果我用guile解释器执行下面的代码,会报错:
guile> (map (lambda (x) (* x 10)) (cons 1 (cons 2 3))) Backtrace: In standard input: 48: 0* [map #<procedure #f (x)> {(1 2 . 3)}] standard input:48:1: In procedure map in expression (map (lambda # #) (cons 1 #)): standard input:48:1: Wrong type argument in position 2: (1 2 . 3) ABORT: (wrong-type-arg)
而用我们自己实现的map2是没有问题的:
guile> (map2 (lambda (x) (* x 10)) (cons 1 (cons 2 3)))
(10 20 . 30)
原文地址:http://xzpeter.org/?p=399
浙公网安备 33010602011771号