类型标识
数据抽象除了单一类型的情况外,还存在多类型的数据抽象,这种情况下需要对不同类型的数据对象表现形式采取通用程式的手段。通用程式可以根据数据对象的具体表现形式将其应用于对应的程式,如何判断数据对象的表现形式呢?可以通过类型标识技术,也就是使标识成为原始数据对象的一部分,在通用程式中便可以从传入的数据对象上取出类型标识并进行判断,最后将不携带标识的原始数据对象应用于相应类型的程式即可。实例如下:
(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)))
(define (contents datum)
(if (pair? datum)
(cdr datum)
(error "Bad tagged datum: CONTENTS" datum)))
(define (rectangular? z)
(eq? (type-tag z) 'rectangular))
(define (polar z) (eq? (type-tag z) 'polar))
;; rectangular representation
(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)))))
;; polar representation
(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) (sin (angle-polar 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)))
数据抽象是在垂直方向上建立抽象屏障,帮助上一级屏障中的程式忽略下一级程式的具体实现,而类型标识能够帮助我们建立水平方向上的抽象屏障,能够在一个系统中兼容同一数据对象的不同表现方式。
数据导向编程
虽然类型标识解决了多种表现方式的问题,但它存在两个缺陷,一是通用选择器需要了解所有的类型以及对应的类型的程式,二是在不同表现方式独立开发情况下容易出现名字冲突的情况(类型标识中的例子在各自的程式中加入了不同的后缀避免这种情况)。这两个缺陷导致在系统中再增加新的表现形式时需要修改原有的代码,这不仅不方便而且容易导致程序出错,所以我们需要一种更加方便的方式实现数据对象的多种表现,它就是数据导向编程。
数据导向编程将通用程式与不同类型对应程式视为二维表格中的一行数据,查找某个类型的具体程式只需要提供类型和程式名称这两个参数,新增的类型也只需要在表中登记对应的类型和程式名称即可,于是对于类型的判断被转换为了对表格的维护。如下图所示:
Table of operations for the complex-number system在 Scheme 中可以通过 put
和 get
操作此表格。要顺利地应用数据导向编程技术,除不同的表现形式代码包开发外,还需要实现 apply-generic
程式,它能够根据程式名称和数据对象类型确定将要执行的程式,并将当前数据对象应用于目标程式。其他的通用选择器只需要基于 apply-generic
实现即可。
(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))))
;; 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)
(define (install-polar-package)
;; internal procedures
(define (magnitude z) (car z))
(define (angle z) (cdr z))
(define (make-from-mag-ang r a) (cons r a))
(define (real-part z) (* (magnitude z) (cos (angle z))))
(define (imag-part z) (* (magnitude z) (sin (angle z))))
(define (make-from-real-imag x y)
(cons (sqrt (+ (square x) (square y)))
(atan y x)))
;; interface to the rest of the system
(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)
(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))))))
;; generic selectors
(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))
消息传递
数据导向编程的关键在于将通用程式的处理对象转变为了操作与类型对应表。这种编程风格主要用于处理需要按类型分发调用对应程式的情况,实际上是通过将操作类型对应表中的数据分成一行一行地处理,因为表格的每行都表示通用操作程式。
另一种策略是通过将表格按列分解的方式处理,按类型分发替换为集成操作,按操作名称分发替换为集成数据对象。按照这种方式能够将相关的事情集中到同一数据对象中,对于矩形数据,就通过接收操作名称的程式和操作对应的程式实现。在这种情况下,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
需要调用具体程式时使用。
这种编程风格称为消息传递。当一个数据对象被视作接收请求的实体,请求本身是操作名称时,此时整个系统就如同消息传递一般。刚才提及的消息传递并不是一个数学技巧,但它是一项在通过通用操作组织系统上十分有用的技术
网友评论