3.3.1 可变的列表结构
在数对上的基本操作cons,car,cdr被能用来组装列表结构和从列表结构中选择一部分,
但是它们没有能力修改列表结构。我们已经使用的列表操作例如append,list,
也是这样的,因为它能使用cons,car,cdr来定义。为了修改列表结构,我们需要新的操作。
图3.12 lists x:((a b) c d)和 y:(e f)
图3.13 在图3.12中的列表上,(set-car! x y)的影响
图3.14 在图3.12中的列表上, (define z (con y (cdr x))) 的影响
图3.15 在图3.12中的列表上,(set-cdr! x y) 的影响
数对的原生的更新子是set-car!和 set-cdr!. set-car!有两个实际参数,第一个参数是
一个数对.它修改这个数对,把car的指针替换成 set-car!的第二个参数的指针.
作为一个例子,假定x被绑定为列表((a b) c d) 和y被绑定为列表(e f),如图3.12.
解释表达式(set-car! x y)修改了x被绑定的数对,把它的car替换为Y。
这个操作的结果显示在图3.13中。x的结构已经被改变了。现在能打印为((e f) c d).
被指针标识着的表示列表的(a b)数对,现在已经从原来的结构中解除了。
比较图3.13和图3.14,这演示了执行(define z (cons y (cdr x)))的结果,
x和y被绑定到图3.12中的原来的列表上。 变量 z现在绑定到由 cons操作
创建的新的数对上。 绑定到x上的列表没有改变。
set-cdr!的操作与set-car!是相似的。仅有的不同是是数对的cdr指针而不是car的指针被替换。
在图3.12上的列表上执行(set-cdr! x y)的影响显示在图3.15上。这里x的cdr的
指针被(e f)的指针替换。而且,(c d)的列表被用作x的cdr,现在被从结构中解除了。
通过创建新的数对,cons构建了新的列表结构, set-car!和 set-cdr!修改了已有的数对。
我们能实现cons,用两个更新,在程序get-new-pair中返回一个新的数对,它不是任何一个
已有的列表结构的一部分。我们得到了新的数对,设置它的car 和cdr指针到特定的对象,
返回这个新的数对作为 cons的结果。
(define (cons x y)
(let ((new (get-new-pair)))
(set-car! new x)
(set-cdr! new y)
new
)
)
练习3.12
在2.2.1部分中介绍了如下的添加列表的程序:
(define (append x y)
(if (null? x)
y
(cons (car x) (append (cdr x) y))
)
)
append通过连续地把x的元素组装到y中,来形成一个新的列表。
append!与append是相似的,但它是一个更新子而不是组装子。
它添加列表通过把它们拼接起来,修改x的最后一个数对,为了
它的cdr现在是y。(当x是空的列表时,调用append!产生一个错误)
(define (append! x y)
(set-cdr! (last-pair x) y) x)
这last-pair是一个程序返回它的参数的最后一个数对:
(define (last-pair x)
(if (null? (cdr x))
x
(last-pair (cdr x))))
考虑如下的交互过程:
(define x (list 'a 'b))
(define y (list 'c 'd))
(define z (append x y))
z
(a b c d)
(cdr x)
<response>
(define w (append! x y))
w
(a b c d)
(cdr x)
<response>
缺失的<response>是什么?画出盒与指针图来解释你的答案。
练习3.13
考虑如下的程序make-cycle,使用在练习3.12中定义的程序last-pair.
(define (make-cycle x)
(set-cdr! (last-pair x) x)
x)
画出盒与指针图,来显示被如下的程序创建的z的结构:
(define z (make-cycle (list 'a 'b 'c)))
如果我们要计算(last-pair z)将发生什么情况?
练习3.14
尽管有一点奇怪,如下的程序是非常有用的:
(define (mystery x)
(define (loop x y)
(if (null? x)
y
(let ((temp (cdr x)))
(set-cdr! x y)
(loop temp x)))
)
(loop x '())
)
loop使用临时的变量temp来保存 x的 cdr的原来的值,因为在下一行的set-cdr!
破坏了cdr。解释 mystery是什么的。假定 v被这个表达式
(define v (list 'a 'b 'c 'd))定义。
画出盒与指针图来表示被v绑定的列表。
假定我们现在解释(define w (mystery v))。
画出盒与指针图来表示解释了这个表达式之后的v和w的结构。
v和 w的值被打印出来的是什么?
*共享与标识
在3.1.3部分中,我们提到了由赋值语句的引入而带来的同一性与可修改性的理论问题.
在实践中,当独立的数对在不同的数据对象中共享时引起的这些问题.例如,考虑如下
形成的结构:
(define x (list 'a 'b))
(define z1 (cons x x))
正如在图3.16中显示的那样,z1是一个数对,它的car和 cdr都
指向相同的数对 x. x是被z1的car和cdr共享的,这是cons被实现的
正常的方式的一种结果.通常情况下,使用cons组装列表,导致数对的
内部联接的结构,许多独立的数对被许多不同的结构共享着.
图3.16 由(cons x x) 形成的z1
图3.17 由(cons (list 'a 'b) (list 'a 'b))形成的z2
与图3.16相反,图3.17显示了被如下的表达式创建的结构:
(define z2 (cons (list 'a 'b) (list 'a 'b)))
在这个结构中,在两个(a b)列表中的数对是唯一的,尽管实际的符号是
共享的.
当被认为是一个列表时,z1和 z2都表示相同的列表((a b) a b).
总之,如果我们的操作仅使用car,cdr,cons的话,共享是完全不能被检测到的。
然而,如果我们允许在列表上的更新子,共享变得很重要了。作为共享制造的不同
的一个例子,考虑如下的程序,它修改了被应用到的结构的car:
(define (set-to-wow!) x)
(set-car! (car x) 'wow)
x)
尽管z1和z2是相同的结构, 应用set-to-wow!到它们后,返回了不同的结果。
在z1上, car和 cdr都改变了,因为 z1的 car和 cdr是相同的数对。
在z2上 car和 cdr是不同的,所以 set-to-wow!仅改变了 car:
z1
((a b) a b)
(set-to-wow! z1)
((wow b) wow b)
z2
((a b) a b)
(set-to-wow! z2)
((wow b) a b)
在列表的结构上检测共享的一种方式是使用判断式eq?,这是在2.3.1部分中,
我们已经介绍了的,作为检测两个符号是否相等的一种方式。
更进一步地说,(eq? x y)测试x和y是否是同一个对象。
(也就是说,x和y 是否是指向相同) 因此,正如z1,z2在图3.16和图3.17中定义的那样,
(eq? (car z1) (car z2))是真,(eq? (cdr z1) (cdr z2))是假。
正如在如下的部分中看到的,我们能探索共享,以极大地扩展数据对象的
这些数据结构能被表示为数对。另一个方面,共享也能是危险的,因为修改
的结构也能影响到其它的结构,这个结构与修改的结构有共享的修改部分。
更新操作set-car!和set-cdr!应该被谨慎地使用;如果我们没有一个我们的
数据对象如何被共享的很好的理解,更新能产生非预期的结果。
练习3.15
画出盒与指针图,来解释set-to-wow!在如上的z1和z2的结构上的效应。
练习3.16
Ben Bitdiddle决定写一个程序来计算数对的数量在任何列表结构中。
“这是容易的”,他的理由是“在任何结构中数对的数量等于其car的中数量和
cdr中的数量的和再加上1。“ 所以他写了如下的程序:
(define (count-pairs x)
(if (not (pair? x))
0
(+ (count-pairs (car x))
(count-pairs (cdr x))
1)
)
)
显示出这个程序是不正确的,特别,画盒与指针图表示列表结构,
它由仅三个数对组成,但是他的程序能返回3,4,7和没有任何返回。
练习3.17
修改出一个练习3.16中的count-pairs程序的正确的版本,
返回任何结构中,独立的数对的数量.(提示:遍历结构,维护一个辅助的数据结构
它被用来记录已经数过的数对)
练习3.18
写一个程序,检查一个列表,来确定它是否包括了环,也就是
通过连续的cdr操作来找列表的结尾时这个程序是否是陷入了无限的循环之中。
练习3.13组装了这样的列表。
练习3.19
重做练习3.18,使用一个算法,这仅使用常数级的空间。
(这要求一个很聪明的思想。)
*更新仅是赋值
当我们介绍了复合的数据时,我们注意到在2.1.3部分中,数对能被单纯地表示为程序:
(define (cons x y)
(define (dispatch m)
(cond ((eq? m 'car) x)
((eq? m 'cdr) y)
(else (error "Undefine operaion --- cons" m))
)
)
dispatch
)
(define (car z) (z 'car))
(define (cdr z) (z 'cdr))
对于可变的数据这相同的情况也是对的。我们能实现可变的数据对象作为程序,
使用赋值和局部状态。例如,我们能扩展如上的数对的实现来处理set-car!和
set-cdr!以一种方式,这种方式与我们实现银行账户在3.1.1部分中使用
make-account的方式是相似的。
(define (cons x y)
(define (set-x! v) (set! x v))
(define (set-y! v) (set! y v))
(define (dispatch m)
(cond ((eq? m 'car) x)
((eq? m 'cdr) y)
((eq? m 'set-car!) set-x!)
((eq? m 'set-cdr!) set-y!)
(else (error "Undefine operaion --- cons" m))
)
)
dispatch
)
(define (car z) (z 'car))
(define (cdr z) (z 'cdr))
(define (set-car! z new-value)
((z 'set-car!) new-value)
z)
(define (set-cdr! z new-value)
((z 'set-cdr!) new-value)
z)
所需要的是仅是赋值,理论上,为了可变数据的行为的计数。只要在语言中,
我们承认set!,我们就引出了所有的问题,不仅是赋值,而是在通常情况下
的可变数据。
练习3.20
画出环境的图,演示如下的表达式的序列的解释过程:
(define x (cons 1 2))
(define z (cons x x))
(set-car! (cdr z) 17)
(car x)
17
使用上面给定的数对的程序性的实现。(比较练习3.11)