83
构构构构构构 构构构构构 构构构构构构构构构构构构构构构 构构构构构构构构构 ,, 构构构构构构 构构构构构构构构构构 构构构构构构构构构构构构构构构构构构构构 ,。, 构构构构构构构构构构构构 构构构构构构 一。 构构构构构构构构构构构 构构 构构构构构构构构构构构构构构构构 一, 构构构构构 构构构构构构构构构 构构构构构构构构构构 构构构构构构构构构构 一:,,, 构构 构构构构构构构构构构构 构构构构构构构构构构构 构构构构构构构构构构 一。,, 构构构构构构构构构构——构构构构

构造数据抽象

  • Upload
    druce

  • View
    45

  • Download
    0

Embed Size (px)

DESCRIPTION

构造数据抽象. 到目前为止,我们操作的都是简单的数值数据,而对我们希望处理的许多问题而言,这种简单数据还不够。许多程序在设计时就是为了模拟复杂的对象,因此他们就常常需要构造起一些计算对象。 与我们在前面所做的事情一样,我们将重点转到各种程序设计语言的另一关键方面:讨论他们所提供的,将数据对象组合起来,形成复合数据的方式,并进一步去考查更复杂的数据。同样的我们还将要看到,与过程抽象相对应的,另一种强有力的设计方法学 —— 数据抽象. 构造数据抽象. 数据抽象 是一种方法学,它使我们能够将一个复合数据对象的使用,与该数据对象怎样由更基本的数据对象构造起来的细节隔离开来。 - PowerPoint PPT Presentation

Citation preview

Page 1: 构造数据抽象

构造数据抽象 到目前为止,我们操作的都是简单的数值数据,而对我们希望处理的许多问题而言,这种简单数据还不够。许多程序在设计时就是为了模拟复杂的对象,因此他们就常常需要构造起一些计算对象。 与我们在前面所做的事情一样,我们将重点转到各种程序设计语言的另一关键方面:讨论他们所提供的,将数据对象组合起来,形成复合数据的方式,并进一步去考查更复杂的数据。同样的我们还将要看到,与过程抽象相对应的,另一种强有力的设计方法学——数据抽象

Page 2: 构造数据抽象

构造数据抽象数据抽象是一种方法学,它使我们能够将一个复合数据对象的使用,与该数据对象怎样由更基本的数据对象构造起来的细节隔离开来。

其基本思想,就是设法构造出一些使用复合对象的程序,使它们就像是在“抽象数据”上操作一样。也就是说,我们的程序中使用数据的方式应该是这样的:除了完成当前工作所必要的东西之外,它们不对所用数据做任何多余的假设。与此同时,一种“具体”数据表示的定义,也应该与程序中使用数据的方式无关。

我们利用一组称为选择函数和构造函数的过程作为这两个部分之间的界面。

Page 3: 构造数据抽象

构造数据抽象实例研究:有理数的算术运算我们希望能做有理数的加减乘除,比较两个有理数是否相等。我们假定已经有了一种从分子和分母构造有理数的方法。进一步假定,如果有了一个有理数,我们有一种方法取得它的分子和分母。现在再假定有关的构造函数和选择函数都可以做为过程使用:(make-rat <n> <d>) 返回一个有理数,其分子是整数 <n> ,分母是整数 <d>(numer <x>) 返回有理数 <x> 的分子(denom <x>) 返回有理数 <x> 的分母

这里使用一种称为按愿望思维的策略。现在我们还没有说有理数将如何表示,也没有说过程 make-rat , numer 和 denom 应如何实现。

利用这三个过程我们就可以实现有理数的运算了

Page 4: 构造数据抽象

构造数据抽象有理数运算过程的实现:加法过程:(define (add-rat x y)

(make-rat (+ (* (numer x) (denom y))

(* (numer y) (denom x)))

(* (denom x) (denom y))))

减法过程:(define (sub-rat x y)

(make-rat (- (* (numer x) (denom y))

(* (numer y) (denom x)))

(* (denom x) (denom y))))

Page 5: 构造数据抽象

构造数据抽象乘法过程:(define (mul-rat x y)

(make-rat (* (numer x) (numer y))

(* (denom x) (denom y))))

除法过程:(define (div-rat x y)

(make-rat (* (numer x) (denom y))

(* (denom x) (numer y))))

相等判断过程:(define (equal-rat? x y)

(= (* (numer x) (denom y))

(* (numer y) (denom x))))

Page 6: 构造数据抽象

构造数据抽象有理数的表示:

我们已经定义了在过程 make-rat , numer 和 denom 基础上的各种有理数的运算过程,而这些基础还没有定义。现在需要某种方式,将一个分子和一个分母粘接起来,构成一个有理数。在实现有理数之前我们先来看看我们所用的语言提供的一种称为序对的复合数据。序对为完成这里的有理数系统提供了一种自然的方式,我们可以将有理数简单的表示为两个整数(分子和分母)的序对。

Page 7: 构造数据抽象

构造数据抽象序对:同样我们只关心序对选择函数和构造函数的形式,而实际过程已由解释器实现了(cons <date1> <date2>), 过程 cons 取两个参数,返回一个包含这两个参数作为其成分的复合数据对象(car <x>) ,其序对中的第一个参数(cdr <x>) ,其序对中的第二个参数注意一个序对也是一个数据对象,可以像基本数据对象一样给它一个名字且操作它。还可用 cons 构造其元素本身就是序对的序对,例如:(define x (cons 1 2))

(define y (cons 3 4))

(define z (cons x y))

(car (car z))

1

(car (cdr z))

2

Page 8: 构造数据抽象

构造数据抽象基于序对的有理数的表示:(define (make-rat n d) (cons n d))

(define (numer x) (car x))

(define (denom x) (cdr x))

为了显示计算结果我们再定义一个打印函数,它以一个有理数作为输入并把它打印出来。(define (print-rat x)

(newline)

(display (numer x))

(display "/") (display (denom x)))

这里我们用到了两个 scheme 的基本内置过程 display 和 newline 。 Display 为打印数据的基本过程, newline 为随后的打印开始一个新行。

Page 9: 构造数据抽象

构造数据抽象(define one-half (make-rat 1 2))

(print-rat one-half)

(define one-third (make-rat 1 3))

(print-rat (add-rat one-half one-third))

(print-rat (mul-rat one-half one-third))

(print-rat (add-rat one-third one-third))

看看上面的过程都会打印出什么?

从打印结果你们发现了什么问题?能否改进?

Page 10: 构造数据抽象

构造数据抽象抽象屏障:在继续讨论之前,让我们首先回顾一下有理数系统的结构:

问题域中的有理数

作为分子和分母的有理数

作为序对的有理数

当然序对也需要实现

使用有理数的程序

add-rat sub-rat …

cons car cdr

make-rat number denom

Page 11: 构造数据抽象

构造数据抽象

其中的水平线表示抽象屏障,它隔离了系统的不同层次,它把使用数据的程序和实现数据的程序分开,使得一层中的过程的实现与其他层的过程的实现完全无关。从作用上看,每一层中的过程构成了所定义的抽象屏障的界面,联系起系统中的不同层次。

这种简单思想有许多优点:

这种方法使程序很容易维护和修改有助于程序的设计,使我们能保留考虑不同实现方式的灵活性,使我们能推迟决策的时间,而又不会阻碍系统其他部分的工作进展。

你能给出几种不同的有理数表示?它们各有什么优缺点?

Page 12: 构造数据抽象

构造数据抽象数据意味着什么前面我们看到在抽象层面上数据可以描述为一组选择函数和构造函数,但是说它就是“由给定的选择函数和构造函数所实现的东西”还是不够的。显然,并不是任意的三个过程都适合作为有理数实现的基础。这里我们还需要保证,如果从整数 n 和 d 构造出一个有理数 x ,那么抽取出的 x的 number 和 denom 并将他们相除,得到的结果应该与 n 除以 d 相同。也就是说这些基本过程的选择是带有约束的。

一般而言,我们总可以将数据定义为一组适当的选择函数和构造函数,以及为使这些过程成为一套合法表示,它们就必须满足的一组特定条件。从上面我们可以得出一个结论:数据其实就是一组定义良好的过程。确实,考虑序对,我们完全可以不用任何数据结构,只用过程实现它。

Page 13: 构造数据抽象

构造数据抽象序对的过程实现:(define (cons x y)

(define (dispatch m)

(cond ((= m 0) x)

((= m 1) y)

(else (error "Argument not 0 or 1 -- CONS" m)))) dispatch)

(define (car z) (z 0))

(define (cdr z) (z 1))

这确实与我们有关数据应该是什么的直观认识大相径庭。这一实例说明可以将过程作为对象去操作,因此就自动地为我们提供了一种表示复合数据的能力。这些东西现在看起来好像只是很好玩,但实际上,数据的过程性表示将在我们的程序设计宝库里扮演一种核心角色。有关的程序设计风格通常称为消息传递。

Page 14: 构造数据抽象

层次性数据和闭包性质

前面已经看到,序对为我们提供了一种用于构造复合数据的基本“粘接剂”,我们可以建立元素本身也是序对的序对,序对的这种能力称为 cons 的闭包性质。一般说,某种数据对象的操作满足闭包性质,就是说,通过它组合起数据对象得到的结果本身还可以通过同样的操作再进行组合。闭包性质是任何一种组合功能的威力的关键要素,它使我们能够建立起层次性的结构。

Page 15: 构造数据抽象

层次性数据和闭包性质层次性结构的表示:图中展示的是序对的标准表示,其中的序对是通过 (cons 1 2) 形成的。

这种称为盒子和指针表示方式中,每个对象表示为一个指向盒子的指针。与基本对象相对应的盒子包含着该对象的表示。复合层次结构的表示:

Page 16: 构造数据抽象

层次性数据和闭包性质序列的表示:我们可以用序对构造一种有用的数据结构——序列,它是一批数据对象的一种有序汇集。采用序对表示序列有很多种方式,在这里我们采用一种标准的方式来表示。下面是对序列 1 , 2 , 3 , 4 的表示:(cons 1

(cons 2

(cons 3

(cons 4 nil))))

通过嵌套的 cons 形成的这样一个序对的序列称为一个表, scheme 为方便表的构造,提供了一个基本操作 list ,如 (list 1 2 3 4) 。一般的:(list <a1> <a2> ... <a3>)

等价于:(cons <a1> (cons <a2> (cons .... (cons <an> nil) ... )))

                                          

Page 17: 构造数据抽象

层次性数据和闭包性质序对的打印:(cons 1 2)

;; (1 . 2)

(cons (cons 1 2) (cons 3 4))

;;((1 . 2) 3 . 4)

(cons 1 (cons 2 (cons 3 nil)))

;;(1 2 3)

                                          

Page 18: 构造数据抽象

层次性数据和闭包性质表的打印:(list 1 2 3 4)

;;(1 2 3 4)

(define one-through-four (list 1 2 3 4))

one-through-four

;: (1 2 3 4)

(car one-through-four)

;: 1

(cdr one-through-four)

;:(2 3 4)

(car (cdr one-through-four))

;:2

(cons 10 one-through-four)

;:(10 1 2 3 4)

                                          

Page 19: 构造数据抽象

层次性数据和闭包性质表操作:利用序对将元素表示为表之后,我们就可以使用常规的程序设计技术,通过”向下 cdr” 表的方式完成对表的各种操作了。返回指定标号的元素操作: list-ref

(define (list-ref items n)

(if (= n 0)

(car items)

(list-ref (cdr items) (- n 1))))

(define squares (list 1 4 9 16 25))

;:(list-ref squares 3)

16

注意:这里我们习惯令表元素的编号从 0 开始。

                                          

Page 20: 构造数据抽象

层次性数据和闭包性质返回表的长度操作: length

(define (length items)

(if (null? items)

0

(+ 1 (length (cdr items)))))

(define odds (list 1 3 5 7))

(length odds)

4

这里我们需要用到 scheme 提供的一个基本操作 null? ,用于检查参数是不是空表。上面的过程是一个递归的过程,空表的 length 为 0 ,任一表的 length 等于其 cdr 的 length 加 1. 给出迭代的 length 过程?

                                          

Page 21: 构造数据抽象

层次性数据和闭包性质连接表操作: append

(define (append list1 list2)

(if (null? list1)

list2

(cons (car list1) (append (cdr list1) list2))))

(append squares odds)

;: (1 4 9 16 25 1 3 5 7)

(append odds squares)

;: (1 3 5 7 1 4 9 16 25)

append 也是一种递归方案,能否给出迭代形式?

                                          

Page 22: 构造数据抽象

层次性数据和闭包性质对表的映射:例:对给定表的所有元素按给定因子做一次放缩(define (scale-list items factor)

(if (null? items)

nil

(cons (* (car items) factor)

(scale-list (cdr items) factor))))

(scale-list (list 1 2 3 4 5) 10)

;:(10 20 30 40 50)

这里我们想抽象出这一具有一般性的想法,将其中公共模式表述为一个高阶过程,这一高阶过程称为 map ,它有一个过程参数和一个表参数,返回将这一过程应用于表中各个元素得到的结果形成的表。

                                          

Page 23: 构造数据抽象

层次性数据和闭包性质对表的映射过程: map

(define (map proc items)

(if (null? items)

nil

(cons (proc (car items))

(map proc (cdr items)))))

(map (lambda (x) (* x x))

(list 1 2 3 4))

(1 4 9 16)

用 map 重新定义 scale-list 过程:(define (scale-list items factor)

(map (lambda (x) (* x factor))

items))

                                          

Page 24: 构造数据抽象

层次性数据和闭包性质层次性结构:((1 2) 3 4) 是通过 (cons (list 1 2) (list 3 4)) 构造出来的,其结构如下:

认识这种元素本身也是序列的序列的另一种方式,是把它们看作树。序列里的元素就是树的分支,而那些本身也是序列的元素就形成了树中的子树。如图:

                                          

Page 25: 构造数据抽象

层次性数据和闭包性质树操作:统计叶子数目操作: count-leaves

(define (count-leaves x)

(cond ((null? x) 0)

((not (pair? x)) 1)

(else (+ (count-leaves (car x))

(count-leaves (cdr x))))))

为了实现这一过程,我们需要一个判断参数是否为序对的函数, scheme 提供了基本过程 pair?

                                          

Page 26: 构造数据抽象

层次性数据和闭包性质对树的映射:(define (scale-tree tree factor)

(cond ((null? tree) nil)

((not (pair? tree)) (* tree factor))

(else (cons (scale-tree (car tree) factor)

(scale-tree (cdr tree) factor)))))

(scale-tree (list 1 (list 2 (list 3 4) 5) (list 6 7))

10)

;:(10 (20 (30 40) 50) (60 70))

该过程以一个数值因子和一颗叶子为数值的树作为参数,返回一颗具有同样形状的树。

                                          

Page 27: 构造数据抽象

层次性数据和闭包性质

下面我们利用前面介绍表的 map 过程重写如下:(define (scale-tree tree factor)

(map (lambda (sub-tree)

(if (pair? sub-tree)

(scale-tree sub-tree factor)

(* sub-tree factor)))

tree))

也已看到这里 map 过程的写法已经没有在用于处理表时那么自然了,究其原因是因为 map 过程并不具备处理层次结构的能力,这样我们在使用是就必须把相关处理写到其参数 proc 的过程里面。自然地,我们希望能有一个能处理这种层次结构的 map 过程,当然其肯定也包含了处理表结构的能力,应为树结构是一种比表更抽象的结构。

                                          

Page 28: 构造数据抽象

层次性数据和闭包性质

重写前面的 map 过程如下:(define (map proc items)

(cond ((null? items) nil)

((pair? items) (cons (map proc (car items))

(map proc (cdr items))))

(else (proc items))))

请利用新定义的 map 过程重写 scale-list 和 scale-tree

                                          

Page 29: 构造数据抽象

层次性数据和闭包性质序列作为一种约定的界面:下面这个例子以一棵树为参数,计算出那些值为奇数的叶子的平方:(define (sum-odd-squares tree)

(cond ((null? tree) 0)

((not (pair? tree))

(if (odd? tree) (square tree) 0))

(else (+ (sum-odd-squares (car tree))

(sum-odd-squares (cdr tree))))))

该过程的大致处理步骤如下:枚举出一棵树的树叶;过滤他们,选出其中的奇数;对选出的每个数求平方;再用 +累积起得到的结果,从 0 开始。

                                          

Page 30: 构造数据抽象

层次性数据和闭包性质这种过程可以很自然地用流过一些级联的处理步骤的信号的方式描述,其中的每个处理步骤实现程序方案中的一个部分,如图所示:

我们从一个枚举器开始,它产生出由个给定的树的所有树叶组成“信号”。这一信号流过一个过滤器,所有不是奇数的数都被删除了。这样得到的信号又通过一个映射,这是一个“转换装置”,它将 square 过程应用于每个元素。这一映射的输出被馈入一个累积器,该装置用 + 将得到的所有元素组合起来,从 0 开始。

                                          

enumerate :tree leaves

filter : odd? map : squareaccumulate :

+,0

Page 31: 构造数据抽象

层次性数据和闭包性质序列操作:要组织好反映上面信号流的结构的程序,最关键的一点就是将注意力集中在处理过程中从一个步骤流向下一个步骤的“信号”。这些信号应该有稳定而灵活的结构,基于其上的基本操作可以组织好这些处理过程。表在这里就是一个良好的选择。基于表结构我们可以很好的实现信号流图中的过程如下:前面的 map 过程可以用来实现信号流图中的映射步骤:过滤一个序列,就是选出其中满足某个给定谓词的元素,实现如下:(define (filter predicate sequence)

(cond ((null? sequence) nil)

((predicate (car sequence))

(cons (car sequence)

(filter predicate (cdr sequence))))

(else (filter predicate (cdr sequence)))))

                                          

Page 32: 构造数据抽象

层次性数据和闭包性质累积工作可以实现如下:(define (accumulate op initial sequence) (if (null? sequence) initial (op (car sequence) (accumulate op initial (cdr sequence)))))

剩下的就是枚举出需要处理的数据序列,对于上面的例子,需要枚举出一棵树的所有树叶,可实现如下:(define (enumerate-tree tree) (cond ((null? tree) nil) ((not (pair? tree)) (list tree)) (else (append (enumerate-tree (car tree)) (enumerate-tree (cdr tree))))))

                                          

Page 33: 构造数据抽象

层次性数据和闭包性质现在,我们就可以像上面的信号流图那些重新构造 sum-odd-squares 了。我们需要枚举一棵树的树叶序列,过滤它,只留下序列中的奇数,求每个元素的平方,而后加起来得到的结果:(define (sum-odd-squares tree) (accumulate + 0 (map square (filter odd? (enumerate-tree tree)))))

将程序表示为一些针对序列的操作,这样做的价值就在于能帮助我们得到模块化的程序设计,也就是说,得到由一些比较独立的片段的组合构成的设计。而模块化结构是控制复杂性的一种威力强大的策略。 在这里,用表实现的序列被作为一种方便的界面,我们可以利用这种界面去组合其各种处理模块,并将程序对数据结构的依赖性局限到不多的几个序列操作上。通过修改这些操作,就可以在序列的不同表示之间转换,并保持程序的整个设计不变。

                                          

Page 34: 构造数据抽象

符号数据

到目前为止,我们已经使用过的所有复合数据,最终都是从数值出发构造起来的。接下来,我们将要扩充所用语言的描述能力,引进将任意符号作为数据的能力。

我们希望构造出表 (a b) ,当然这里不能用 (list a b) ,因为这将构造出 a 和 b 的值的表,而不是其符号本身。为了能构造出这些符号,我们使用的语言里就需要有一种新元素,它具有引用数据对象的能力。在 scheme 里我们通过一个单引号来引用一个对象。

Page 35: 构造数据抽象

符号数据现在我们可以通过单引号区分一个符号和他们的值了:(define a 1)(define b 2)(list a b);:(1 2) (list 'a 'b);: (a b)(list 'a b);: (a 2)引号也可以用于复合对象:(car '(a b c));: a(cdr '(a b c));: (b c)

Page 36: 构造数据抽象

符号数据对符号的操作:为了能对符号进行各种操作,我们的语言提供了一个基本过程 eq? ,这个过程以两个符号作为参数,检查它们是否为同样的符号。

我们利用 eq? 实现一个称为 memq 的有用过程,它以一个符号和一个表为参数。如果这个符号不包含在这个表里,就返回假;否则返回该表的由这个符号的第一次出现开始的那个子集:(define (memq item x) (cond ((null? x) false) ((eq? item (car x)) x) (else (memq item (cdr x)))))(memq 'apple '(pear banana prune));:false(memq 'apple '(x (apple sauce) y apple pear));:(apple pear)

Page 37: 构造数据抽象

符号数据解释器在求值下面各个表达式时将打印出什么?(list 'a 'b 'c)

(list (list 'george))

(cdr '((x1 x2) (y1 y2)))

(cadr '((x1 x2) (y1 y2)))

(pair? (car '(a short list)))

(memq 'red '((red shoes) (blue socks)))

(memq 'red '(red shoes blue socks))

Page 38: 构造数据抽象

符号数据实例研究:符号求导 我们希望实现一个过程,以一个代数表达式和一个变量作为参数,返回这个表达式相对于该变量的导数。如:参数为 ax2+bx+c 和 x ,它应该返回 2ax+b 。 当然,一个强有力的符号求导系统的开发是很困难的,为了使有关讨论简单化,我们在这里考虑一个非常简单的符号求导程序,它处理的表达式都是由对于两个参数的加和乘运算构造起来的。这样其求导可以通过下面几条规则完成:

0 c x

1

( )

( )( ) ( )

dc

dxdx

dxd u v du dv

dx dx dxd uv dv du

u vdx dx dx

当 是一个常量,或者一个与 不同的变量

Page 39: 构造数据抽象

符号数据为了能在一个过程中体现这些规则,我们用一下按愿望思维,就像在前面设计有理数的实现所做的那样。假设现在已经有了一种表示代数表达式的方式,那么我们需要判断该表达式是否为一个和式、乘式、常量、变量,还需要能提取出表达式的各个部分。也就是说我么在这里需要一组构造函数、选择函数和谓词作为我们求导程序的界面:(variable? e) e 是变量吗?(same- variable? v1 v2) v1 和 v2 是统一变量吗?(sum? e) e 是和式吗?(addend e) e 的被加数(augend e) e 的加数(make-sum a1 a2) 构造 a1 与 a2 的和式(product? e) e 是乘式吗?(multiplier e) e 的被乘数(multiplicand e) e 的乘数(make-product m1 m2) 构造 m1 与 m2 的乘式

Page 40: 构造数据抽象

符号数据利用上面这些过程,以及判断表达式是否数值的基本过程 number ?,我们就可以将各种求导规则用下面的过程表达出来了:

(define (deriv exp var)

(cond ((number? exp) 0)

((variable? exp)

(if (same-variable? exp var) 1 0))

((sum? exp)

(make-sum (deriv (addend exp) var)

(deriv (augend exp) var)))

Page 41: 构造数据抽象

符号数据 ((product? exp)

(make-sum

(make-product (multiplier exp)

(deriv (multiplicand exp) var))

(make-product (deriv (multiplier exp) var)

(multiplicand exp))))

(else

(error "unknown expression type -- DERIV" exp))))

过程 deriv 包含了一个完整的求导算法。因为它是基于抽象数据表述的,因此,无论我们如何选择代数表达式的具体表示,只要设计了一组正确的选择函数和构造函数,这个过程都可以工作。

Page 42: 构造数据抽象

符号数据代数表达式的表示:我们可以想出很多用表结构表示代数表达式的方法,一种特别直截了当的选择,是采用 Lisp 里面表示组合式的那种带括号的前缀形式:变量就是符号,他们可以用基本谓词 symbol? 判断:(define (variable? x) (symbol? x))

两个变量相同就是表示它们的符号相互 eq? :(define (same-variable? v1 v2)

(and (variable? v1) (variable? v2) (eq? v1 v2)))

和式与乘式都构造为表:(define (make-sum a1 a2) (list '+ a1 a2))

(define (make-product m1 m2) (list '* m1 m2))

和式就是第一个元素为符号 + 的表:(define (sum? x) (and (pair? x) (eq? (car x) '+)))

Page 43: 构造数据抽象

符号数据被加数是表示和式的表里的第二个元素:(define (addend s) (cadr s))

加数是表示和式的表里的第三个元素:(define (augend s) (caddr s))

乘是就是第一个元素为符号 * 的表:(define (product? x) (and (pair? x) (eq? (car x) '*)))

被乘数是表示乘式的表里的第二个元素:(define (multiplier p) (cadr p))

乘数是表示乘式的表里的第三个元素:

有了代数表达式的具体表示以后我们就能实际使用求导程序了:(define (multiplicand p) (caddr p));: (deriv '(+ x 3) 'x);: (deriv '(* x y) 'x);: (deriv '(* (* x y) (+ x 3)) 'x)

Page 44: 构造数据抽象

符号数据有了代数表达式的具体表示以后,我们就能实际使用求导程序了:(define (multiplicand p) (caddr p))

;:(+ 1 0)

(deriv '(+ x 3) 'x)

;: (+ (* x 0) (* 1 y))

(deriv '(* x y) 'x);: (deriv '(* (* x y) (+ x 3)) 'x)

;: (+ (* (* x y) (+ 1 0))

(* (+ (* x 0) (* 1 y))

(+ x 3)))

我们可以看到程序产生的结果是对的,但是他们没有经过化简。当然,我们希望程序能够知道 x*0=0,1*y=y 以及 0+y=y 。我们可以想想在有理数中碰到的情形。同样我们可以通过修改选择函数和构造函数实现这一化简,完全不必修改 deriv 。

Page 45: 构造数据抽象

符号数据新的 make-sum 过程,当两个求和对象都是数时,返回他们的和,当其中一个为零时,返回另外一个对象:(define (make-sum a1 a2)

(cond ((=number? a1 0) a2)

((=number? a2 0) a1)

((and (number? a1) (number? a2)) (+ a1 a2))

(else (list '+ a1 a2))))

该过程用到了 =number? ,它检查某个表达式是否等于一个给定的数。(define (=number? exp num)

(and (number? exp) (= exp num)))

Page 46: 构造数据抽象

符号数据与 make-sum类似,修改后的 make-prouct 过程,设法引进下面的规则:0 与任何对象的乘积都是 0 , 1 与任何东西的乘积总是那个对象(define (make-product m1 m2)

(cond ((or (=number? m1 0) (=number? m2 0)) 0)

((=number? m1 1) m2)

((=number? m2 1) m1)

((and (number? m1) (number? m2)) (* m1 m2))

(else (list '* m1 m2))))

下面使这一新版过程对前面三个例子的结果:;:1

;: y

;: (+ (* x y) (* y (+ x 3)))

虽然大有改观,但第三个例子还是说明,这不足以满足我们认为的最简形式。思考这里所谓的最简形式的含义,它有没有统一的风格?

Page 47: 构造数据抽象

抽象数据的多重表示

抽象屏障是控制复杂性的强有力的工具。通过对数据对象基础表示的屏蔽,我们就可以将设计一个大程序的任务,分割为一组可以分别处理的较小任务。但是,这种类型的数据抽象还不够强大有力。从一个角度看,对于一个数据对象可能存在多种有用的表示方法,而且我们也可能希望所设计的系统能处理多种表示形式。例如:复数可以表示为两种等价的形式:直角坐标(实部和虚部)和极坐标(模和幅角)。有时采用直角坐标形式更合适,有时极坐标形势更方便。所以,我们可能希望一个系统能同时采用两种表示形式。而其中的过程可以对具有任意表示形式的复数工作。接下来我们就通过实现这一复数系统来展示抽象数据的多重表示技术。

Page 48: 构造数据抽象

抽象数据的多重表示利用一个按愿望思维,我们希望有如下 2 个构造函数和 4 个选择函数:make-frome-real-imag: 返回一个采用实部和虚部描述的复数;make-frome-mag-ang: 返回一个采模和幅角描述的复数;real-part: 以一个复数为参数,返回其实部;imag-part: 以一个复数为参数,返回其虚部;magnitude: 以一个复数为参数,返回它的模;angle :以一个复数为参数,返回其幅角;

这些过程的性质是:(make-from-real-imag (real-part z) (imag-part z))(make-from-mag-ang (magnitude z) (angle z))产生出的复数都等于 z 。

利用这些构造函数和现在函数,我们就可以实现复数的运算了。

Page 49: 构造数据抽象

抽象数据的多重表示我们希望,加减法采用实部和虚部的方式描述,而乘除法采用模和幅角的方式描述:(define (add-complex z1 z2) (make-from-real-imag (+ (real-part z1) (real-part z2)) (+ (imag-part z1) (imag-part z2))))(define (sub-complex z1 z2) (make-from-real-imag (- (real-part z1) (real-part z2)) (- (imag-part z1) (imag-part z2))))(define (mul-complex z1 z2) (make-from-mag-ang (* (magnitude z1) (magnitude z2)) (+ (angle z1) (angle z2))))(define (div-complex z1 z2) (make-from-mag-ang (/ (magnitude z1) (magnitude z2)) (- (angle z1) (angle z2))))

Page 50: 构造数据抽象

抽象数据的多重表示为了完成这一复数包,我们必须选择一种表示方法,而且必须基于基本的数值和基本的表结构。有两种显而易见的方式完成这一工作:可以将复数按“直角坐标形式”表述为一个有序对(实部,虚部),或者“极坐标形式”表示为序对(模,幅角)。假设现在已经设计出了这两种复数系统的具体表示形式。基于直角坐标的表示形式:(define (real-part z) (car z))(define (imag-part z) (cdr z))(define (magnitude z) (sqrt (+ (square (real-part z)) (square (imag-part z)))))(define (angle z) (atan (imag-part z) (real-part z)))(define (make-from-real-imag x y) (cons x y))(define (make-from-mag-ang r a) (cons (* r (cos a)) (* r (sin a))))上面用到了一个反正切函数 atan ,它取两个参数 y 和 x ,返回正切是 y/x 的角度。参数的符号决定角度所在的象限

Page 51: 构造数据抽象

抽象数据的多重表示基于极坐标的表示形式:(define (real-part z)

(* (magnitude z) (cos (angle z))))

(define (imag-part z)

(* (magnitude z) (sin (angle z))))

(define (magnitude z) (car z))

(define (angle z) (cdr z))

(define (make-from-real-imag x y)

(cons (sqrt (+ (square x) (square y)))

(atan y x)))

(define (make-from-mag-ang r a) (cons r a))

数据抽象的规则保证了复数的加减乘除运算的同一套实现对这两种表示都能正常工作。

Page 52: 构造数据抽象

抽象数据的多重表示究竟应该选择哪一种表示方式?我们处在一个尴尬的境地了,因为这两表示形式同样的有用,我们希望同时获得这两种表示形式的优点。能不能在同一个系统里包含两种不同的表示形式?答案是能,但我们需要一种方式,将极坐标形式的数据和直角坐标形式的数据区分。完成这种区分的一种方式,就是在每个复数里包含一个类型标志部分——用符号 rectangular或者 polar 。此后如果我们需要操作一个复数,借助于这个标志就可以确定应该使用的选择函数了。为了能对带标志数据进行各种操作,我们将假定有过程 type-tag 和 contents, 它们分别从数据对象中提取出类型标志和实际内容。还要假定有一个过程它以标志和实际内容为参数,生成出一个带标志的数据对象。实现这些的直接方式就是采用普通表结构:(define (attach-tag type-tag contents) (cons type-tag contents))(define (type-tag datum) (if (pair? datum) (car datum) (error "Bad tagged datum -- TYPE-TAG" datum)))

Page 53: 构造数据抽象

抽象数据的多重表示(define (contents datum) (if (pair? datum) (cdr datum) (error "Bad tagged datum -- CONTENTS" datum)))利用这些过程,我们就可以定义出谓词 rectangular? 和 polar?, 他们分别辨识直角坐标表示和极坐标表示的复数:(define (rectangular? z) (eq? (type-tag z) 'rectangular))(define (polar? z) (eq? (type-tag z) 'polar))有了类型标志之后,我们可以修改前面的代码,使两种不同表示能够共存于同一个系统中了。在不同的表示形式中,构造一个复数时,总为它加上对应的标志。此外还必须保证所用的过程名不冲突。这样应为过程名也加上相应的后缀。经过修改后的两种表示形式如下:

Page 54: 构造数据抽象

抽象数据的多重表示修改后的直角坐标表示:(define (real-part-rectangular z) (car z))(define (imag-part-rectangular z) (cdr z))(define (magnitude-rectangular z) (sqrt (+ (square (real-part-rectangular z)) (square (imag-part-rectangular z)))))(define (angle-rectangular z) (atan (imag-part-rectangular z) (real-part-rectangular z)))(define (make-from-real-imag-rectangular x y) (attach-tag 'rectangular (cons x y)))(define (make-from-mag-ang-rectangular r a) (attach-tag 'rectangular (cons (* r (cos a)) (* r (sin a)))))

Page 55: 构造数据抽象

抽象数据的多重表示修改后的极坐标表示:(define (real-part-polar z)

(* (magnitude-polar z) (cos (angle-polar z))))

(define (imag-part-polar z)

(* (magnitude-polar z) (sin (angle-polar z))))

(define (magnitude-polar z) (car z))

(define (angle-polar z) (cdr z))

(define (make-from-real-imag-polar x y)

(attach-tag 'polar

(cons (sqrt (+ (square x) (square y)))

(atan y x))))

(define (make-from-mag-ang-polar r a)

(attach-tag 'polar (cons r a)))

Page 56: 构造数据抽象

抽象数据的多重表示基于上面的两种表示我们需要实现通用型函数,它们首先检查参数的标志,而后去调用处理该类数据的适当过程。(define (real-part z) (cond ((rectangular? z) (real-part-rectangular (contents z))) ((polar? z) (real-part-polar (contents z))) (else (error "Unknown type -- REAL-PART" z))))(define (imag-part z) (cond ((rectangular? z) (imag-part-rectangular (contents z))) ((polar? z) (imag-part-polar (contents z))) (else (error "Unknown type -- IMAG-PART" z))))

Page 57: 构造数据抽象

抽象数据的多重表示(define (magnitude z)

(cond ((rectangular? z)

(magnitude-rectangular (contents z)))

((polar? z)

(magnitude-polar (contents z)))

(else (error "Unknown type -- MAGNITUDE" z))))

(define (angle z)

(cond ((rectangular? z)

(angle-rectangular (contents z)))

((polar? z)

(angle-polar (contents z)))

(else (error "Unknown type -- ANGLE" z))))

我们可以看到前面的复数加减乘除的运算仍然无须改动。

Page 58: 构造数据抽象

抽象数据的多重表示

最后,我们还需要确定采用那种构造复数的方式。一种合理的选择是:在手头有实部和虚部时采用直角坐标表示,有模和幅角时采用极坐标表示:(define (make-from-real-imag x y)

(make-from-real-imag-rectangular x y))

(define (make-from-mag-ang r a)

(make-from-mag-ang-polar r a))

Page 59: 构造数据抽象

抽象数据的多重表示这样回过头来看我们所定义的过程,我们用一种非常自然的鉴别方式,成功的把复数的两种不同表示形式包含到同一个复数包中了。这样得到的系统具有如下图所示的结构:

使用复数的程序

复数算术包

直角坐标表示 极坐标表示

add-complex sub-complex mul-complex div-complex

real-part imag-partMagnitude angle

Page 60: 构造数据抽象

抽象数据的多重表示数据导向的程序设计和可加性: 基于类型的分派,在系统设计中是一种获得模块性的强有力的策略。而另一方面,同前面那样实现的分派有两个显著的弱点。一是,其中的这些通用型界面过必须知道所有的不同表示,如果现在希望增加另一种表示,我们就必须将这一新表示方法标识为一种新类型,而且要在每个通用界面过程里增加一个句子,检查这一新类型,并对这种表示形式使用适当的选择函数。还有一个弱点就是,即使这些独立的表示形式可以分别设计,我们也必须保证在整个系统里不存在两个名字相同的过程。 由此带来的问题是,这种实现通用型界面的技术不具有可加性。因为每次增加一种新表示时,实现通用选择函数的人都必须修改它们的过程,而那些做独立表示界面的人也必须修改其代码,以避免名字冲突问题。很多时候这种做法会带来极大的不变,而且还很容易引进错误。接下来我们介绍将系统进一步模块化的方法,通过使用一种称为数据导向的程序设计的编程技术。

Page 61: 构造数据抽象

抽象数据的多重表示事实上,我们正是在处理一个二维表格,其中的一个维上包含着所有的可能操作,另一个维就是所有的可能类型。如图:

数据导向的程序设计就是一种使程序能直接利用这种表格工作的程序设计技术。它用操作名和参数类型的组合到表格中查找,以便找出应该调用的适当过程,并将这一过程应用于参数的内容。这样把一种新的表示包加入系统里,我们就不需要修改任何现存的过程,而只要在这个表格里添加一些新的项目即可。

Page 62: 构造数据抽象

抽象数据的多重表示为了实现这一计划,现在假定有两个过程 put 和 get ,用于处理这种操作—类型表格:(put <op> <type> <item>) :将项 <item> 加入表格中,以 <op> 和 <type> 作为这个表项的索引(get <op> <type>) :在表中查找和对应的项,如果找到就返回找到的项,否则就返回假。

下面我们要说明,这种数据导向的程序设计可以如何用于复数系统。我们在定义完复数的表示的程序包后,要通过向表格中加入一些项的方式,告诉系统如何去操作直角坐标形式表示的数,这样就建立起了与系统其它部分的界面。我们通过安装的方式向表中加入我们要提供出来的过程。

Page 63: 构造数据抽象

抽象数据的多重表示直角坐标表示包的安装过程:(define (install-rectangular-package)

;; internal procedures

(define (real-part z) (car z))

(define (imag-part z) (cdr z))

(define (make-from-real-imag x y) (cons x y))

(define (magnitude z)

(sqrt (+ (square (real-part z))

(square (imag-part z)))))

(define (angle z)

(atan (imag-part z) (real-part z)))

(define (make-from-mag-ang r a) (cons (* r (cos a)) (* r (sin a))))

Page 64: 构造数据抽象

抽象数据的多重表示;; interface to the rest of the system

(define (tag x) (attach-tag 'rectangular x))

(put 'real-part '(rectangular) real-part)

(put 'imag-part '(rectangular) imag-part)

(put 'magnitude '(rectangular) magnitude)

(put 'angle '(rectangular) angle)

(put 'make-from-real-imag 'rectangular

(lambda (x y) (tag (make-from-real-imag x y))))

(put 'make-from-mag-ang 'rectangular

(lambda (r a) (tag (make-from-mag-ang r a))))

'done)

Page 65: 构造数据抽象

抽象数据的多重表示我们可以看到这里所有的内部过程,与最开始定义的过程完全一样,将它们与系统的其他部分建立联系时,也不需要做任何修改。进一步说,由于这些过程定义都是上述安装过程内部的东西,故而完全不必担心它们的名字会与直角坐标程序包外面的其他过程的名字互相冲突。所以完全没有必要在加入系统时修改包内过程的名字。

写出极坐标包的安装过程?

Page 66: 构造数据抽象

抽象数据的多重表示 复数算术的选择函数通过一个通用的名为 apply-generic 的“操作”过程访问有关表格,这个过程将通用型操作应用于一些参数。 apply-generic 在表格中用操作名和参数类型查找,如果找到,就去应用查找中得到的过程:(define (apply-generic op . args)

(let ((type-tags (map type-tag args)))

(let ((proc (get op type-tags)))

(if proc

(apply proc (map contents args))

(error

"No method for these types -- APPLY-GENERIC“

(list op type-tags))))))

Page 67: 构造数据抽象

抽象数据的多重表示利用 apply-generic ,各种通用型选择函数可以定义如下:

(define (real-part z) (apply-generic 'real-part z))

(define (imag-part z) (apply-generic 'imag-part z))

(define (magnitude z) (apply-generic 'magnitude z))

(define (angle z) (apply-generic 'angle z))

最后,我们同样可以从表中提取出构造函数,用到包之外的程序中:(define (make-from-real-imag x y)

((get 'make-from-real-imag 'rectangular) x y))

(define (make-from-mag-ang r a)

((get 'make-from-mag-ang 'polar) r a))

Page 68: 构造数据抽象

抽象数据的多重表示带有通用型操作的系统: 我们已经看到过几个不同的算术运算包:语言内部的基本算术 (+、 -、*、 /) ,有理数算术 (add-rat,sub-rat,mul-rat,div-rat), 以及复数算术包。现在我们要使用数据导向构造起一个算术运算包,将它们全部都结合进去,我们将看到如何使用同样的思想,不但定义出能够在不同表示上的通用操作,还能定义针对不同参数种类的通用型操作。 下面是这一系统的结构图。我们要注意其中的各抽象屏障。

Page 69: 构造数据抽象

抽象数据的多重表示使用数的程序

通用型算术包

复数算术有理数算术 常规算术

直角坐标 极坐标

add sub mul div

add-rat sub-ratmul-rat div-rat

add-complex sub-complexmul-complex div-complex

+ - * /

Page 70: 构造数据抽象

抽象数据的多重表示首先是从某些使用“数值”的人的观点看,无论提供给它的数是什么,都只存在过程 add、 sub、mul 和 div 。这一界面将使那些使用数的程序能以一种统一的方式,访问相互分离的算术运算包。

通用型算术过程的定义如下:(define (add x y) (apply-generic 'add x y))

(define (sub x y) (apply-generic 'sub x y))

(define (mul x y) (apply-generic 'mul x y))

(define (div x y) (apply-generic 'div x y))

接下来我们将为每种数附着一个类型标志,以便通用型过程能够根据其参数的类型完成到某个适用的程序包的分派

Page 71: 构造数据抽象

抽象数据的多重表示 首先我们从安装处理常规数的包开始,对这种数采用的标志是符号 scheme-number 。这个包的算术运算都是基本过程因为每个操作都有两个参数,所以用表 (scheme-number scheme-number) 作为表格中的键值去安装它们:(define (install-scheme-number-package)

(define (tag x)

(attach-tag 'scheme-number x))

(put 'add '(scheme-number scheme-number)

(lambda (x y) (tag (+ x y))))

(put 'sub '(scheme-number scheme-number)

(lambda (x y) (tag (- x y))))

(put 'mul '(scheme-number scheme-number)

(lambda (x y) (tag (* x y))))

Page 72: 构造数据抽象

抽象数据的多重表示(put 'div '(scheme-number scheme-number)

(lambda (x y) (tag (/ x y))))

(put 'make 'scheme-number

(lambda (x) (tag x))) 'done)

Scheme 数值包的用户可以通过下面过程,创建带标志的常规数:(define (make-scheme-number n)

((get 'make 'scheme-number) n))

接下来我们安装有理数包,采用的标志是 rational 。由于具有可加性,我们可以直接把前面的有理数代码作为这个包的内部过程,完全不必做任何修改:

Page 73: 构造数据抽象

抽象数据的多重表示(define (install-rational-package)

;; internal procedures

(define (numer x) (car x))

(define (denom x) (cdr x))

(define (make-rat n d)

(let ((g (gcd n d)))

(cons (/ n g) (/ d g))))

(define (add-rat x y)

(make-rat (+ (* (numer x) (denom y))

(* (numer y) (denom x)))

(* (denom x) (denom y))))

Page 74: 构造数据抽象

抽象数据的多重表示(define (sub-rat x y)

(make-rat (- (* (numer x) (denom y))

(* (numer y) (denom x)))

(* (denom x) (denom y))))

(define (mul-rat x y)

(make-rat (* (numer x) (numer y))

(* (denom x) (denom y))))

(define (div-rat x y)

(make-rat (* (numer x) (denom y))

(* (denom x) (numer y))))

Page 75: 构造数据抽象

抽象数据的多重表示;; interface to rest of the system

(define (tag x) (attach-tag 'rational x))

(put 'add '(rational rational)

(lambda (x y) (tag (add-rat x y))))

(put 'sub '(rational rational)

(lambda (x y) (tag (sub-rat x y))))

(put 'mul '(rational rational)

(lambda (x y) (tag (mul-rat x y))))

(put 'div '(rational rational)

(lambda (x y) (tag (div-rat x y))))

(put 'make 'rational

(lambda (n d) (tag (make-rat n d))))

'done)

Page 76: 构造数据抽象

抽象数据的多重表示接着我们可以安装另一个处理复数的类似程序包,采用 complex 做标志。在创建时,我们要从表格里抽取出操作 make-from-real-imag 和 make-from-mag-ang 。

(define (install-complex-package)

;; imported procedures from rectangular and polar packages

(define (make-from-real-imag x y)

((get 'make-from-real-imag 'rectangular) x y))

(define (make-from-mag-ang r a)

((get 'make-from-mag-ang 'polar) r a))

Page 77: 构造数据抽象

抽象数据的多重表示;; internal procedures

(define (add-complex z1 z2)

(make-from-real-imag (+ (real-part z1) (real-part z2))

(+ (imag-part z1) (imag-part z2))))

(define (sub-complex z1 z2)

(make-from-real-imag (- (real-part z1) (real-part z2))

(- (imag-part z1) (imag-part z2))))

(define (mul-complex z1 z2)

(make-from-mag-ang (* (magnitude z1) (magnitude z2))

(+ (angle z1) (angle z2))))

(define (div-complex z1 z2)

(make-from-mag-ang (/ (magnitude z1) (magnitude z2))

(- (angle z1) (angle z2))))

Page 78: 构造数据抽象

抽象数据的多重表示 ;; interface to rest of the system

(define (tag z) (attach-tag 'complex z))

(put 'add '(complex complex)

(lambda (z1 z2) (tag (add-complex z1 z2))))

(put 'sub '(complex complex)

(lambda (z1 z2) (tag (sub-complex z1 z2))))

(put 'mul '(complex complex)

(lambda (z1 z2) (tag (mul-complex z1 z2))))

(put 'div '(complex complex)

(lambda (z1 z2) (tag (div-complex z1 z2))))

(put 'make-from-real-imag 'complex

(lambda (x y) (tag (make-from-real-imag x y))))

(put 'make-from-mag-ang 'complex

(lambda (r a) (tag (make-from-mag-ang r a))))

'done)

Page 79: 构造数据抽象

抽象数据的多重表示

在复数包之外的程序可以从实部和虚部出发构造复数,也可以从模和幅角出发。请注意这里如何将原先定义在直角坐标和极坐标包里集成的过程导出,放入复数包中,又如何从这里导出送给外面的世界。

(define (make-complex-from-real-imag x y)

((get 'make-from-real-imag 'complex) x y))

(define (make-complex-from-mag-ang r a)

((get 'make-from-mag-ang 'complex) r a))

Page 80: 构造数据抽象

抽象数据的多重表示

这里描述的是一个具有两层标志的系统。一个典型的复数如直角坐标表示的 3+4i, 如图所示。外层标志 (complex) 用于将这个数引导到复数包,一旦进入复数包,下一个标志 (rectangular) 就会引导这个数进入直角坐标表示包。在一个大型的复杂系统里可能有许多层次,每层与下一层次之间的连接都借助于一些通用型操作,当一个数据对象被“向下”传输时,用于引导它进入适当程序包的最外层标志被剥除,下一层次的标志变成可见,并将被用于下一次分派。

Page 81: 构造数据抽象

抽象数据的多重表示 我们至今定义的所有运算,都把不同数据类型看作相互完全分离的东西。现在我们将面临如下问题:定义出能够跨过类型界限的操作。如:完成一个复数和一个常规数的加法。 这样,要实现这一想法,我们可以设计出一些强制过程,他们能把一个类型的对象转换到另一类型的等价对象。下面是一个将常规数值转换为一个复数的过程:(define (scheme-number->complex n)

(make-complex-from-real-imag (contents n) 0))

我们将这些强制过程安装到一个特殊的强制表格中,用两个类型的名字作为索引:(put-coercion 'scheme-number 'complex scheme-number->complex)

Page 82: 构造数据抽象

抽象数据的多重表示 这里假定存在着用于操纵这个表格的 put-coercion 和 get-coercion 过程。一般的,这个表格里的某些格子将是空的,因为并不是任何数据对象转换到另一个类型都被允许。如:我们不能将复数转换为常规数值。一旦将上述转换表格装配好,我们就可以修改 apply-generic 过程,得到一种处理强制的统一方法。(define (apply-generic op . args)

(let ((type-tags (map type-tag args)))

(let ((proc (get op type-tags)))

(if proc

(apply proc (map contents args))

(if (= (length args) 2)

(let ((type1 (car type-tags))

(type2 (cadr type-tags))

Page 83: 构造数据抽象

抽象数据的多重表示 (a1 (car args))

(a2 (cadr args)))

(let ((t1->t2 (get-coercion type1 type2))

(t2->t1 (get-coercion type2 type1)))

(cond (t1->t2

(apply-generic op (t1->t2 a1) a2))

(t2->t1

(apply-generic op a1 (t2->t1 a2)))

(else

(error "No method for these types“

(list op type-tags))))))

(error "No method for these types“

(list op type-tags)))))))