2.4.3 面向数据的编程与添加

2.4.3 面向数据的编程与添加

检查一个数据的类型和调用合适的程序的通用化的策略被称之为在类型上的分发。
这是一种在系统设计中,很强有力的实现模块化的策略。另一个方面,在2.4.2部分实现的分发中,
有两个重要的缺点。一个缺点是通用化的接口程序(real-part,imag-part,magnitude,angle)必须知道
所有的不同的表示方法。例如,假定我们要集成一种新的复数表示方式到我们的复数系统之中。我们需要
用一种类型来标识这种新的表示方式,然后在每个通用的接口程序中加上一个语句来检查新的类型,并且
为了新的表示方式应用合适的选择子。

这个技术的另一个缺点是即使独立的表示方式被单独的设计,我们仍然必须保证在整个系统中没有命名上的冲突。
这也是在2.4.1部分中Ben 和Alyssa不得不修改他们的原始的程序名称。

这些缺点之下的问题是实现通用接口的技术不具有可累加性。当有一个新的表示方式被加入时,
人们实现通用化的选择子程序必须修改那些程序一次。并且人们接口化的单独的表示方式
必须修改他们的代码以避免名称冲突。在这些案例中的任何一个,修改代码是自然而然的事,但是代码必须被修改的天衣无缝,
而这是不方便和出错的源泉。对于这个复数的系统而言这并不是一个大的问题,但是,
假定对于复数不是有两个而是有数以百计的不同的表示方式,假定在抽象数据的接口中
有很多的通用化的选择子要维护,假定,事实上没有一个程序员能知道所有的接口程序和所有的表示方式。
在大规模的基于数据管理的系统这样的程序之中问题是真实存在的,也是必须被解决的。

我们所需要的是对系统的设计更进一步地模块化的方法。这种方法是被一种编程技术实现的,这种技术称之为面向数据编程。
为了理解面向数据编程是怎么工作的,开始于注意到的一个事实,这个事实是我们处理的通用化的操作的集合
与不同的类型的集合有共同的部分,在效果上看,是一个二维的表格包括了一个轴是可能的操作,另一个轴是可能的类型。
在表格中的项是某种操作在某种类型时的程序。在前面的部分中开发的复数系统中,相对应的内容是操作名称,
数据类型,在通用化的接口程序中的条件语句中被调用的实际的程序。
但是相同的信息能被组织在一个表格中。如下面的图2.22所示。

面向数据的编程是直接使用这样的表格的设计程序的技术。如前,我们实现的机制是复数算术的代码与两个表示方式的包作接口。
这个程序包有一些程序,任何一个程序都执行一个显示的类型的分发。 这里我们将实现接口为一个单独的程序,
它找出在表格中的操作名称和参数类型的组合,以找到要应用的正确的程序,然后把它应用到参数的内容上。
如果我们这么做,然后,加一个新的表示方法的程序包到系统中时,我们不再需要修改任何已经存在的程序。我们仅需要添加新的
程序项到表格之中。

                          类型
                   极坐标表示          |    平面坐标表示
                 ——————————————————————
  操作     取实部  |real-part-polar    | real-part-rectangular
           取虚部  |imag-part-polar    | imag-part-rectangular
           取长度  |magnitude-polar    | magnitude-rectangular
           取角度  |angle-polar        | angle-rectangular
图2.22 复数系统的操作的表格

为了实现这个计划,我们假定有两个程序,put和get来操作 有‘操作’和‘类型’的表格。

*  (put <op> <type> <item>)
  在表格中安装<item>,以<op> 和<type> 为索引。

*  (get <op> <type>)
   在表格中,查找<op> <type>的项 并且返回找到的项。如果没有找到项,返回假。

现在,我们能够假定 put 和get已经包含于我们的语言之中了。在第三章中(3.3.3部分的
练习3.24) 我们将看到在操作表格方面怎么实现这些和其它的一些操作。

这里有在复数系统中面向数据编程是如何被使用的。Ben开发了平面坐标的表示法,
正如他之前的做法,他实现了代码。他定义了一系列的程序,或者是软件包,对系统
其它部分的接口。通过向表中加入口项,告诉系统如何操作平面坐标的数。通过调用如下的程序
这是完成的结果:

(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))))
  ;;
  (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
)

注意的是这里的内部程序与2.4.1部分中Ben单独工作时写的程序是一样的。
没有必要为了与系统的其它部分的接口而修改。进而,由于这些程序定义
是安装程序的内部程序,Ben不需要担心与平面坐标的软件包的外部程序的
命名冲突。为了与系统的其它部分的接口,Ben在操作名称real-part和类型
rectangular中安装了real-part程序,对于其它的选择子,情况是相似的。
接口也定义了为外部系统所使用的组装子。这与Ben的内部的定义的组装子
是相同的,除了添加的标签。

Alyssa的极坐标的软件包是相似的。

(define (install-polar-package)
   ;; internal procedure
   (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))
    ;;
   (define (tag x) (attach-tag 'polar x))
  (put 'real-part '(polar) real-part)
  (put 'imag-part '(polar) imag-part)
  (put 'magnitude '(polar) magnitude)
  (put 'angle     '(polar) angle)
  (put 'make-from-real-imag 'polar
       (lambda (x y) (tag (make-from-real-imag x y))))
  (put 'make-from-mag-ang 'polar
       (lambda (r a) (tag (make-from-mag-ang) r a)))
 'done
)

尽管Ben和Alyssa都仍然使用他们的原来的程序定义,彼此的程序名
也相同,这些定义现在是不同的程序的内部的,(见1.1.8部分),所以没有命名冲突。

复数的算术的选择子读取表格,使用一个通用的操作的程序,这个程序叫做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))   
             )
        )    
   )
)

使用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))

注意的是,如果一种新的表示法被添加到系统中,这没有任何的修改。

我们也能从表格中抽取到被软件包的外部程序使用的组装子,这些组装子
组装复数使用实部和虚部 或者是用长度和角度。正如2.4.2部分中,
当我们有实部和虚部时我们组装了平面坐标的复数,当我们有长度和角度时,
我们组装了极坐标的复数。

(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)
)

练习2.73
2.3.2部分中描述了一个执行符号微分的程序。

(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) ))
            ((product? exp) (make-sum (make-product (multiplier exp)
                                                    (deriv (multiplicand exp) var))                                                                     (make-product (deriv (multiplier exp) var)
                                                    (multipicand exp))))
            <更多的规则添加在此处>
       (else (error "unknown expression type" exp))))

我们能认为这个程序是执行了被微分的表达式的类型的分发。在这种情况下,
已知数的类型是代数的操作符例如加号,正在执行的操作是deriv.通过重写基本的微分程序
我们能把这个程序转换成面向数据的风格。

(define (deriv exp var)
  (cond ((number? exp) 0)
        ((variable? exp) (if (same-variable? exp var) 1  0))
        (else ((get 'deriv (operator exp)) (operands exp) var))
  )
)

(define (operator exp) (car exp))
(define (operands exp) (cdr exp))

a. 解释上述程序做什么。为什么我们没有把判断式number? 和same-variable?放入面向数据的分发之中?

b. 写加法和乘法的微分的程序,还有被上述的程序所使用的辅助代码,需要在表格中安装这些代码。

c. 选择你想要的任何其它的微分规则,例如练习2.56中的指数的情况。在面向数据的系统中安装它。

d. 在这个简单的代数操作程序中表达式的类型是代数操作符号。假定,然而,我们以相反的方式索引程序,
为了在微分程序中分发行如下:

((get (operator exp) 'deriv) (operands exp) var)

微分的程序系统需要相应的修改什么内容?

练习2.74

不知足的企业是一个高度的权力下放的联合性的公司,它由分布于全世界的大量的独立的部门组成。公司的计算机工具
已经使用一个智能的网络接口模式连接,来使得整个网络以一台计算机的面目出现于任何一个人面前。不知足的总裁,
在她的首次尝试中,探索网络的能力,来从部门的文件中抽取管理性的信息,不可能发现的是,尽管所有的部门文件已经
在scheme语言中以数据结构实现了,不同的部门使用不同的特定性的数据结构。一个部门经理的会议,很着急的要求,
找到一个策略,来集成满足领导的需要的文件,这需求要保护部门的存在的自治权。

显示这样的策略结合面向数据的编程,如何被实现。作为一个例子,假定
任何一个部门的员工记录包括一个单独的文件,这个文件包括记录的集合,这记录
以员工姓名为键。集合的结构随部门的不同而改变。进而,任何一个员工的记录,本身是一个集合,
(它的结构随部门的不同而不同),这个集合包括被键的信息,在标识之下,例如地址和工资。特别:

a. 为领导实现一个get-record程序,这个程序检索一个特定的员工记录,从一个特定的员工文件。
这个程序应该能够应用到任何一个部门的文件。解释独立的部门文件应该如何被结构化?特别是,什么
类型信息必须被提供?

b. 为领导实现一个get-salary程序,这个程序从任何一个目标部门的员工文件中,
  返回一个给定的员工记录的工资信息。为了让这个操作有效,记录应该被如何结构化?

c.为领导实现一个find-employee-record程序,这个程序应该能搜索所有有部门文件,为了找到
给定员工的记录,并且返回记录。假定,这个程序以员工姓名和所有的部门文件的列表为参数。

d.当不知足的接了一个新的企业时,为了集成新的员工信息到中央系统中,必须做什么修改呢?

*  消息传递
面向数据的编程的重要的思想是在程序中,通过使用显式的处理操作与类型
的表格,例如图2.22中的表格,能处理通用化的操作。
在2.4.2部分中我们使用的编程的风格是在类型上组织必要的分发,通过
让任何一种操作有它自己的分发。在效果上看,这分解了操作与类型的表格为
行数据,任何一个通用的操作的程序表示为表格的一行数据。

另一个可选的实现策略是分解表格为列,替换使用智能的操作,即分发数据的类型,
为智能的数据对象,即分发操作名称。我们能这么做,通过安排一个数据对象,
例如一个平面坐标的数,被表示为一个程序,这个程序的输入要求操作名称,
执行显示的操作。在这个原则下,make-from-real-imag能写成如下的样子:

(define (make-from-real-imag x y)
   (define (dispatch op)
      (cond ((eq? op 'real-part) x)
            ((eq? op 'imag-part) y)
     ((eq? op 'magnitude) (sqrt (+ (square x) (square y))))
     ((eq? op 'angle) (atan y x))
     (else (error "Unknown op -- MAKE-FROM-REAL-IMAG" op))    
      )
   )
   dispatch
)

相应的apply-generic程序,应用一个通用化的操作到一个实际参数上,现在
简单地给数据对象喂操作名称,让对象这个工作。

(define (apply-generic op arg) (arg op))

注意到,make-from-real-imag程序的返回值是一个程序,
是内部的dispatch程序。当apply-generic要求一个操作被执行时,
这是那个被调用的程序。

编程的这种风格被叫做消息传递。这个名称来自于那个景像。即一个数据对象
是一个接收请求的操作名称作为消息的实体。在2。1。3部分中,
我们已经看到一个例子,在那里我们看到了在没有数据对象仅有程序的情况下,
cons,car,cdr是如何被定义的。这里我们看到的是消息传递不是一个数学的绝窍,
而是一个有用的用通用操作来组织系统的技术。在这章的余下的部分中,我们
继续使用面向数据的编程,而不是消息传递,来讨论通用化的算术操作。
在第三章中,我们将返回到消息传递,并且我们将看到它能成为一个强有力的
结构化的模拟程序的工具。

练习2.75
以消息传递的编程风格,实现组装子make-from-mag-ang程序。
这个程序应该与上述的make-from-real-imag 相似。


练习2.76
作为一个大型的带有通用化操作的系统在进化,数据对象的新类型
或者是新操作可能是需要的。对于三个策略中的任何一个,带有显式分发的
通用化操作,面向数据的风格,消息传递的风格,为了加新的类型
或者是新的操作,描述一下对于系统必须要做的修改。在新的类型必须被常常
添加的情况下,对于一个系统而言,哪个组织是最合适的?
在新的操作必须被常常添加的情况下,对于一个系统而言,哪个组织是最合适的?

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81503385