2.5.2 组合不同类型的数据

 2.5.2 组合不同类型的数据
我们已经看到怎么定义一个统一的算术系统,这个系统包括了普通数,复数,
有理数和其它类型的数。但是我们忽略一个重要的问题。我们已经定义的这些操作,
对待不同的数据类型,认为它们是完全独立的。因此,对于加法有单独的程序包,也就是说,
两个普通数的加法,或者是两种复数的加法。我们还没有考虑到的内容是这样一个事实,即
定义一个跨类型的边界的操作是很有意义的,例如一个复数和一个普通数的加法。
我们已经有巨大的努力来产生我们的程序部分之间的边界,为了能够被独立地开发与单独的理解。
我们要介绍跨类型的操作在一些被严格控制的方式。为了我们能支持它们却没有很严重地破坏我们的模块边界。

处理跨类型操作的一种方式是有效的。这种方式是设计一个不同的程序
为操作类型的任何一种可能的组合。例如,我们能扩展复数软件包,为了让它
提供一个复数加上一个普通数的程序,再使用标签(complex scheme-number)
把安装到表格中。

;; 包含在复数的软件包中。
(define (add-complex-to-schemenum z x)
(make-from-real-imag (+ (real-part z) x) (imag-part z)))

(put 'add '(complex scheme-number)
      (lambda (z x) (tag (add-complex-to-schemenum z x))))

这种技术能工作,但是它是累赘的。使用这样的系统,引入一个新的类型,
它的代价不仅是那个类型的程序的软件包中的组装子程序,还有实现跨类型
操作的程序的组装和安装。比起定义类型本身需要的操作,这能很容易地编写
更多的代码。这个方法也干扰了我们有累加性地单独的软件包的组合的能力,
或者说至少限制了单独的软件包的实现的扩展,是因为软件包要用到其它软件包。
例如,如上的例子,处理复数和普通数的跨类型操作应该是复数软件包的职责,
这似乎是合理的。组合有理数和复数的情况,然而,可能在复数软件包实现,
有理数软件包实现,或者是一些需要使用这两个软件包中的一些操作的
第三方的软件包。在很多的软件包和许多的跨类型操作的系统的设计之中,
在软件包之间划分职责的条理分明的策略的制定能成为一个压倒一切的
最重要的任务。

*强制类型转换
在完全不相关的类型上进行完全不相关的操作的通常的情况之下,
实现显式的跨类型操作,尽管可能是累赘的,但还是我们能
希望的最好的办法了。幸运的是,通过利用附加的结构,我们常常能
做的更好。这种附加的结构可能潜伏在我们的类型系统中。不同的数据
类型常常不是完全独立的,可能有方式把一种类型的对象视为其它的类型。
这个过程叫做强制类型转换。例如,我们被要求计算一个普通数和一个复
数,我们能把普通数看作是虚部为0的复数。这把问题转换成处理两个复数。
这能用复数软件包以常规的方式进行处理。

总之,我们能通过设计强制类型转换的程序来实现这个思想。强制类型转换的程序
能把一个类型的对象转换成另一个类型的等价的对象。这有一个强制的程序,
它把一个给定的普通数,转换成一个有实数和虚部的复数:

(define (scheme-number->complex n)
  (make-complex-from-real-imag (contents n) 0)
)

我们在一个特定的强制类型转换的表格中,安装地这些强制的程序,以下面的
两个类型的名称为索引。

(put-coercion 'scheme-number 'complex scheme-number->complex)

(对于操作这个表格,我们假定有put-coercion 和get-coercion 程序是可用的。)
通常在表格的一些位置上是空的,因为通常情况下不可能强制地把任意一个类型下的
随意的数据对象转为所有的其它的类型。例如,不能把一个普通的复数强制转换为
一个普通的数。所以在表格中,没有包括程序complex->scheme-number。

一旦强制的表格已经做好了,通过修改2.4.3部分中的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))
    (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))
        )
     )       
       )
  )
)

强制模式,相比如上的定义显式的跨类型的操作的方法来说,有许多的优势。
尽管我们仍需要写相关的类型的强制程序(对于一个有N个类型的系统,可能有N*N个程序)
我们需要写的仅是类型的每一个数对一个强制程序而不是类型与操作的组合的集合中的每个元素
有一个不同的程序。我们这里计算的内容是这个事实,它是适合的类型间的转换仅依赖于
这些类型本身,而不是被应用的操作。

另一个方面,有了强制的模式的应用可能还不足够。即使当没有对象可以转换成其它的类型,
它仍然可能存在把这两个对象都转换成第三种类型,来执行操作。为了处理这种复杂性,
并且仍然保留我们的程序中的模块性,通常有必要构建系统利用类型间的关系的进一步的结构,
正如我们将要讨论的内容。

*类型的层次
如上显示的强制模式依赖于类型的数对之间的自然关系的存在性。
在不同的类型如何彼此关联方面,常常有更全局的结构。例如,假定
我们构建了一个通用的算术系统来处理整数,有理数,实数,和复数。
在这样的系统中,把一个整数视为一个有理数的特例是自然的,进而它是
实数的一个特例,更是一个复数的特例。这种情况,我们叫它类型的层次。
例如,整数是有理数的子类型,(能应用于有理数的任何操作能自动地被应用
于一个整数。)相反的,我们说有理数形成了整数的一个超类型。我们这有的特别的层次
是一个很简单的类型,它的任何一个类型最多只有一个超类型,最多只有一个子类型。
这种结构,叫做一个塔,如图2.25所示。

复数
^
        |
实数
        ^
        |
有理数
^
        |
整数

图2.25 类型的塔

如果我们有一个塔结构,那么,我们能极大地简化加一个新类型到
层级中的问题,对于我们仅需要指定这个新类型如何被嵌入到它上面
超类与它如何成为下面的类型的超类。例如,如果我们要加一个整数到
复数,我们不需要显示定义一个特定的强制程序integer->complex.
代替的是,我们定义一个整数能如何被转换成一个有理数,一个有理数是
如何被转换成一个实数,和一个实数如何被转换成一个复数。然后,
我们允许系统通过这些步骤把一个整数转换成一个复数。然后,加两个复数。

以如下的方式,我们能重新设计我们的apply-generic程序,对于任何一种类型,
我们需要提供一个升级程序,这个程序把那个类型的对象在塔中升一级。
那么当系统需要操作不同的类型的对象时,它能连续地升级低级的类型,直到
所有的对象在塔中处理同一层级。(练习2.83和练习2.84关注了实现这个
策略的细节。)

一个塔的另一个优势是我们能很容易地实现一个理念,它是每个类型继承了
它的超类型定义的所有的操作。例如,我们没有提供一个特定的程序为了找一个
整数的实部。我们应该无需要期望取实部的操作为整数定义,是基于这个事实。
它是整数是复数的子类型。在一个塔中,我们能以一种统一的方式,安排这发生。
通过修改apply-generic.如果需求的操作没有被给定的类型直接定义,我们能
升级对象到它的超类型,再试。我们因此而爬塔,正如我们转换我们的参数,
直到我们找到了一个层级,期望的操作能被执行,或者来到了塔顶
(这种情况下我们得放弃了)。

一个塔相对于一个更加通用的层级结构的另一个优势是它给了我们一个简单的方式,来把
一个数据对象化简到最简单的表示形式。例如,如果我们把2+3i 和4-3i 相加的话,
得到的答案是整数6而不是复数6+0i,这就更好了。练习2.85讨论了实现这种
降级操作的一种方式。(有难度的是我们需要一种通用的方式来区别哪些对象能被降级
例如6+0i,哪些对象不能被降级,例如 6+2i)。

多边形

———————————————————————————————
|                    |
   三角形 四边形
| |
———————————— —————————————
| | | |
等腰三角形 直角三角形 梯形 风筝形
| | | |
———————————————— | | |
| | | | |
等边三角形 等腰直角三角形 平行四边形 |
| |—————
| |
矩形 菱形
| |
———————————————

正方形

图2。26 几何对象的类型之间的关系

*层次的缺陷
正如我们已经看到的那样,如果在我们的系统中的数据类型,能被自然地安排到一个塔中,
这能极大地简化我们处理在不同类型上的通用化操作的问题。不幸的是,这通常是不可能的。
见图2。26所示,一个更复杂的混合的类型的安排,这个图显示了不同的几何图形的类型
之间的关系。我们看到,通常情况下,一个类型可能有多个子类型。例如,三角形和四边形
都是多边形的子类型。此外,一个类型可能有多个超类型。例如,一个等腰直角三角形可能
被视为一个等腰三角形或者是一个直角三角形。这种多超类型的问题是特别地麻烦,
因为它意味着在层级中,没有一个唯一的路径来升级类型。在应用一个操作到一个对象时
找到正确的超类型可能包括了在程序(例如apply-generic)的某部分中进行
在整个类型网络中认真地搜索。因为一个类型通常有多个子类型,在类型层级中向下强制
一个值时,也有类似的问题。在大型系统的设计中处理大量的不相关的类型,
并且仍然保留了模块性是非常困难的事。它是一个当前有很多的研究的领域。

练习2.81
Louis Reasoner已经注意到 apply-generic 可能尝试强制参数为其它类型即使
它们已经有了相同的类型。因此,他有理由,我们需要把程序放在强制表格中。
为了任何一种类型的强制参数到它自己的类型。例如,除了如上述的强制程序
scheme-number>complex,他可以做如下的程序:

(define (scheme-number->scheme-number n) n)
(define (complex->complex z) z)
(put-coercoin 'scheme-number 'scheme-number
   scheme-number->scheme-nubmer)
(put-coercoin 'complex 'complex complex->complex)

a.随着Louis的强制程序的安装,如果apply-generic调用两个参数,它们的类型
都是scheme-number,或者都是complex,在表格中针对它们的类型,没有找到这个操作,
会发生什么情况?例如,我们已经定义了一个通用的指数操作:

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

把一个指数程序放在软件包scheme-number中,没有在其它的软件包中:

;;把上述程序安装到软件包scheme-number
(put 'exp '(scheme-number scheme-nubmer)
(lambda (x y) (tag (expt x y)))) ;使用原生的expt

如果我们调用exp以两个复数为参数,会发生什么情况?

b.关于同类型的参数的强制转换有一些事不得不做,Louis是对的吗,
apply-generic程序工作是对的吗?
c.如果两个参数有相同的类型,为了不尝试强制类型转换,修改apply-generic。

练习2.82
显示如何通用化apply-generic来处理强制类型转换在多个参数的通用情况。
一个策略是试图强制所有的参数为第一个参数的类型,然后是第二个参数,等等。
这个策略是通用的情况下是不充分的(像如上的两个参数的版本)给出这个情况下的一
个例子。(提示:考虑这种情况,即有一些合适的混合类型的操作在表格中有表示,
却没有被试过)


练习2.83
假定你设计一个通用的算术系统,处理类型的塔,如图2。25所示。
整数,有理数,实数,复数。对于任何一种类型(除了复数)设计一个程序,
升级那个类型的对象在塔中的一个级别。显示如何安装一个通用的升级程序,
让它能工作在任何一种类型(除了复数)。

练习2.84
使用练习2.83中的升级程序,修改程序apply-generic,通过连续使用升级程序,
使它的参数有相同的类型,正如在这部分的讨论那样。你将需要一个测试方式,来确定
塔中哪个类型的层级更高。以这种方式做来兼容系统的其它部分,并且在塔加上
新的层级时没有导致问题。

练习2.85
这部分提到了一个方法用来化简一个数据对象,这个方法通过在类型的塔中尽可能地降低
类型的层级。设计一个程序drop,为了在练习2。83中描述的塔来完成这任务。
在某些通用的方式中,关键是决定,一个对象能否被降级。例如,复数1。5+0i
能被降级为有理数,复数1+0i能被降级为整数,但是2+3i根本不能被降级。
为了确定一个对象能否被降级,这有一个计划:开始于一个通用的操作project,
它推一个对象到塔中。例如,对一个复数做操作,包括了扔掉虚部。如果当我们
处理它时,它被改变了,我们把结果升级回原来的类型。显示如何在细节上实现
这个思想,通过写一个drop程序,尽可能地扔掉一个对象。你将需要设计各种
工程的操作。并且安装这个通用化的操作在系统中。你也需要利用一个通用化的相等
判断式,例如在练习2。79中描述的。最后你能使用drop来重写练习2。84
中的apply-generic,来简化它的答案。

练习2.86
假定我们要处理复数,它的实部,虚部,长度和角度能是普通数,有理数,或者
是我们想要加入到系统中的其它数。为了系统需要适应这,描述和实现这个修改。
你将不得不定义操作,例如sine和cosine针对普通数和有理数是通用的。

猜你喜欢

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