We have seen how to define a unified arithmetic system that encompasses
ordinary numbers, complex numbers, rational numbers, and any other type of
number we might decide to invent, but we have ignored an important issue. The
operations we have defined so far treat the different data types as being
completely independent. Thus, there are separate packages for adding, say, two
ordinary numbers, or two complex numbers. What we have not yet considered is
the fact that it is meaningful to define operations that cross the type
boundaries, such as the addition of a complex number to an ordinary number. We
have gone to great pains to introduce barriers between parts of our programs so
that they can be developed and understood separately. We would like to
introduce the cross-type operations in some carefully controlled way, so that
we can support them without seriously violating our module boundaries.
我们已经看到如何定义一个统一的算术系统,将普通数、复数、有理数以及任何我们可能想发明的其他数类型都纳入其中,但我们忽略了一个重要问题。迄今定义的操作将不同的数据类型视为完全独立的,因此分别有独立的包来做两个普通数或两个复数的加法。我们尚未考虑的是,定义跨越类型边界的操作也是有意义的,例如将一个复数与一个普通数相加。我们费了很大力气在程序的各部分之间设立屏障,以便它们可以独立地开发和理解。我们希望以某种经过仔细控制的方式引入跨类型操作,从而在不严重破坏模块边界的前提下支持这类操作。
One way to handle cross-type operations is to design a different procedure for
each possible combination of types for which the operation is valid. For
example, we could extend the complex-number package so that it provides a
procedure for adding complex numbers to ordinary numbers and installs this in
the table using the tag (complex scheme-number):
处理跨类型操作的一种方法是,为操作有效的每种类型组合分别设计一个过程。例如,我们可以扩展复数包,令其提供一个将复数与普通数相加的过程,并以标签 `(complex scheme-number)` 为键将其安装到表中:
This technique works, but it is cumbersome. With such a system, the cost of
introducing a new type is not just the construction of the package of
procedures for that type but also the construction and installation of the
procedures that implement the cross-type operations. This can easily be much
more code than is needed to define the operations on the type itself. The
method also undermines our ability to combine separate packages additively, or
at least to limit the extent to which the implementors of the individual packages
need to take account of other packages. For instance, in the example above, it
seems reasonable that handling mixed operations on complex numbers and ordinary
numbers should be the responsibility of the complex-number package. Combining
rational numbers and complex numbers, however, might be done by the complex
package, by the rational package, or by some third package that uses operations
extracted from these two packages. Formulating coherent policies on the
division of responsibility among packages can be an overwhelming task in
designing systems with many packages and many cross-type operations.
Subheading: Coercion
这种技术虽然可行,却十分繁琐。在这样的系统中,引入一种新类型的代价不仅仅是构造该类型的过程包,还需要构造并安装实现跨类型操作的过程,其代码量很容易远超定义该类型本身操作所需的代码。这种方法还削弱了我们以可加方式组合各独立包的能力,至少会扩大各包实现者需要顾及其他包的范围。例如在上面的例子中,处理复数与普通数的混合操作,由复数包负责似乎是合理的;然而有理数与复数的组合,既可由复数包处理,也可由有理数包处理,还可以由某个从这两个包中提取操作的第三方包来处理。在设计拥有众多包和众多跨类型操作的系统时,制定关于包间职责划分的连贯策略可能是一项极为艰巨的任务。
小节标题:强制类型转换
In the general situation of completely unrelated operations acting on
completely unrelated types, implementing explicit cross-type operations,
cumbersome though it may be, is the best that one can hope for. Fortunately,
we can usually do better by taking advantage of additional structure that may
be latent in our type system. Often the different data types are not
completely independent, and there may be ways by which objects of one type may
be viewed as being of another type. This process is called
coercion.
For example, if we are asked to arithmetically combine an ordinary number with
a complex number, we can view the ordinary number as a complex number whose
imaginary part is zero. This transforms the problem to that of combining two
complex numbers, which can be handled in the ordinary way by the
complex-arithmetic package.
在操作与类型之间完全没有关联的一般情形下,实现显式的跨类型操作虽然繁琐,却已是我们所能期望的最好做法。幸运的是,通过利用类型系统中可能潜藏的额外结构,我们通常能做得更好。不同的数据类型往往并非完全独立,一种类型的对象有时可以被视为另一种类型的对象。这一过程称为强制类型转换 (coercion)。例如,若要对一个普通数和一个复数执行算术运算,我们可以将普通数视为虚部为零的复数,从而将问题转化为两个复数的运算,进而用复数算术包以通常的方式处理。
In general, we can implement this idea by designing coercion procedures that
transform an object of one type into an equivalent object of another type.
Here is a typical coercion procedure, which transforms a given ordinary number
to a complex number with that real part and zero imaginary part:
一般地,我们可以通过设计强制转换过程来实现这一思路——将一种类型的对象转换为另一种类型的等价对象。以下是一个典型的强制转换过程,它将给定的普通数转换为以该数为实部、虚部为零的复数:
We install these coercion procedures in a special coercion table, indexed under
the names of the two types:
我们将这些强制转换过程安装到一张专门的强制转换表中,以两个类型的名称为索引:
(We assume that there are put-coercion and get-coercion
procedures available for manipulating this table.) Generally some of the slots
in the table will be empty, because it is not generally possible to coerce an
arbitrary data object of each type into all other types. For example, there is
no way to coerce an arbitrary complex number to an ordinary number, so there
will be no general complex->scheme-number procedure included in the
table.
(我们假设已有 `put-coercion` 和 `get-coercion` 过程可用于操作这张表。)一般而言,表中某些位置将是空的,因为并非总能将某种类型的任意数据对象强制转换为所有其他类型。例如,不存在将任意复数强制转换为普通数的通用方法,因此表中不会有 `complex->scheme-number` 这样的通用过程。
Once the coercion table has been set up, we can handle coercion in a uniform
manner by modifying the apply-generic procedure of 2.4.3.
When asked to apply an operation, we first check whether the operation is
defined for the arguments’ types, just as before. If so, we dispatch to the
procedure found in the operation-and-type table. Otherwise, we try coercion.
For simplicity, we consider only the case where there are two
arguments. We check the
coercion table to see if objects of the first type can be coerced to the second
type. If so, we coerce the first argument and try the operation again. If
objects of the first type cannot in general be coerced to the second type, we
try the coercion the other way around to see if there is a way to coerce the
second argument to the type of the first argument. Finally, if there is no
known way to coerce either type to the other type, we give up. Here is the
procedure:
一旦建立了强制转换表,我们就可以通过修改 2.4.3 节的 `apply-generic` 过程,以统一的方式处理强制类型转换。当被要求应用某个操作时,我们首先像以前一样检查该操作是否针对参数的类型有定义;若有,则分派到操作-类型表中找到的过程。否则,我们尝试强制转换。为简单起见,我们只考虑有两个参数的情形。我们检查强制转换表,看第一个类型的对象是否可以强制转换为第二个类型;若可以,则对第一个参数执行强制转换,再次尝试该操作。若第一个类型的对象一般不能强制转换为第二个类型,则反向尝试,看是否能将第二个参数强制转换为第一个参数的类型。最后,若不存在已知的任何一方向另一方强制转换的方式,则放弃。以下是该过程:
This coercion scheme has many advantages over the method of defining explicit
cross-type operations, as outlined above. Although we still need to write
coercion procedures to relate the types (possibly n
2 procedures for a
system with n types), we need to write only one procedure for each pair of
types rather than a different procedure for each collection of types and each
generic operation. What we are counting on here is the fact
that the appropriate transformation between types depends only on the types
themselves, not on the operation to be applied.
与上文概述的显式跨类型操作方法相比,这种强制转换方案有许多优点。尽管我们仍然需要编写强制转换过程来关联各类型(对于含有 n 种类型的系统,可能需要多达 n² 个过程),但对于每对类型,我们只需编写一个过程,而不是为类型的每种组合与每个通用操作分别编写一个过程。我们在这里所倚重的事实是:类型之间适当的转换方式仅取决于类型本身,而与所要应用的操作无关。
On the other hand, there may be applications for which our coercion scheme is
not general enough. Even when neither of the objects to be combined can be
converted to the type of the other it may still be possible to perform the
operation by converting both objects to a third type. In order to deal with
such complexity and still preserve modularity in our programs, it is usually
necessary to build systems that take advantage of still further structure in
the relations among types, as we discuss next.
Subheading: Hierarchies of types
另一方面,某些应用可能超出了强制转换方案的适用范围。即使待组合的两个对象都无法转换为对方的类型,有时仍可以通过将两者都转换为第三种类型来完成操作。为了应对这种复杂性并在程序中保持模块性,通常需要构建能够利用类型之间关系中更深层结构的系统,这正是我们下面要讨论的内容。
小节标题:类型的层次结构
The coercion scheme presented above relied on the existence of natural
relations between pairs of types. Often there is more “global” structure in
how the different types relate to each other. For instance, suppose we are
building a generic arithmetic system to handle integers, rational numbers, real
numbers, and complex numbers. In such a system, it is quite natural to regard
an integer as a special kind of rational number, which is in turn a special
kind of real number, which is in turn a special kind of complex number. What
we actually have is a so-called
hierarchy of types, in which, for
example, integers are a
subtype of rational numbers (i.e., any
operation that can be applied to a rational number can automatically be applied
to an integer). Conversely, we say that rational numbers form a
上面介绍的强制转换方案依赖于类型对之间自然关系的存在。通常,不同类型的相互关系具有更为“全局性”的结构。例如,假设我们要构建一个处理整数、有理数、实数和复数的通用算术系统。在这样的系统中,将整数视为有理数的一种特例,有理数视为实数的一种特例,实数视为复数的一种特例,是完全自然的。我们实际上拥有的是所谓的类型的层次结构 (hierarchy of types):在这个层次结构中,整数是有理数的子类型 (subtype)(即任何可以应用于有理数的操作都可以自动应用于整数)。反过来,我们称有理数构成整数的
supertype of integers. The particular hierarchy we have here is of a
very simple kind, in which each type has at most one supertype and at most one
subtype. Such a structure, called a
tower, is illustrated in
Figure 2.25.
Figure 2.25: A tower of types.
超类型 (supertype)。这里的特定层次结构是一种非常简单的形式:每种类型至多有一个超类型,至多有一个子类型。这样的结构称为塔 (tower),如图 2.25 所示。
图 2.25:类型的塔。
If we have a tower structure, then we can greatly simplify the problem of
adding a new type to the hierarchy, for we need only specify how the new type
is embedded in the next supertype above it and how it is the supertype of the
type below it. For example, if we want to add an integer to a complex number,
we need not explicitly define a special coercion procedure
integer->complex. Instead, we define how an integer can be transformed
into a rational number, how a rational number is transformed into a real
number, and how a real number is transformed into a complex number. We then
allow the system to transform the integer into a complex number through these
steps and then add the two complex numbers.
如果我们有了塔结构,就能大大简化向层次结构中添加新类型的问题,因为我们只需说明新类型如何嵌入到其上方紧邻的超类型,以及它如何成为其下方类型的超类型。例如,若要将一个整数与一个复数相加,无需显式定义专门的强制转换过程 `integer->complex`,只需定义整数如何转换为有理数、有理数如何转换为实数、实数如何转换为复数,然后让系统通过这些步骤将整数转换为复数,再将两个复数相加即可。
We can redesign our apply-generic procedure in the following way: For
each type, we need to supply a raise procedure, which “raises” objects
of that type one level in the tower. Then when the system is required to
operate on objects of different types it can successively raise the lower types
until all the objects are at the same level in the tower. (Exercise 2.83
and Exercise 2.84 concern the details of implementing such a strategy.)
我们可以按如下方式重新设计 `apply-generic` 过程:对每种类型,我们需要提供一个 `raise` 过程,将该类型的对象在塔中“提升”一层。这样,当系统需要对不同类型的对象执行操作时,可以对较低层的类型逐步提升,直到所有对象处于塔中的同一层级。(练习 2.83 和练习 2.84 涉及实现这一策略的具体细节。)
Another advantage of a tower is that we can easily implement the notion that
every type “inherits” all operations defined on a supertype. For instance,
if we do not supply a special procedure for finding the real part of an
integer, we should nevertheless expect that real-part will be defined
for integers by virtue of the fact that integers are a subtype of complex
numbers. In a tower, we can arrange for this to happen in a uniform way by
modifying apply-generic. If the required operation is not directly
defined for the type of the object given, we raise the object to its supertype
and try again. We thus crawl up the tower, transforming our argument as we go,
until we either find a level at which the desired operation can be performed or
hit the top (in which case we give up).
塔结构的另一个优点是,我们可以方便地实现“每种类型都继承其超类型上定义的所有操作”这一概念。例如,若我们没有为求整数实部提供专门的过程,仍然应当期望 `real-part` 对整数有定义——这正是因为整数是复数的子类型。在塔结构中,我们可以通过修改 `apply-generic` 以统一的方式实现这一点:若所需操作没有直接针对给定对象的类型定义,就将该对象提升到其超类型再次尝试。如此沿着塔向上攀爬,在上升过程中对参数进行转换,直到找到能够执行所需操作的层级,或到达塔顶(此时放弃)为止。
Yet another advantage of a tower over a more general hierarchy is that it gives
us a simple way to “lower” a data object to the simplest representation. For
example, if we add 2
+
3
i to 4
−
3
i, it would be nice to obtain the
answer as the integer 6 rather than as the complex number 6
+
0
i.
Exercise 2.85 discusses a way to implement such a lowering operation.
(The trick is that we need a general way to distinguish those objects that can
be lowered, such as 6
+
0
i, from those that cannot, such as 6
+
2
i.)
Subheading: Inadequacies of hierarchies
塔形结构相对于更一般的层次体系还有另一个优势:它为我们提供了一种将数据对象“降低”到最简表示的简便方式。例如,若将 2 + 3i 与 4 − 3i 相加,最好能以整数 6 而非复数 6 + 0i 的形式给出结果。练习 2.85 讨论了一种实现这种降低操作 (lowering) 的方式。(其中的关键在于:我们需要一种通用方法,来区分那些可以降低的对象——如 6 + 0i——和那些不能降低的对象——如 6 + 2i。)
If the data types in our system can be naturally arranged in a tower, this
greatly simplifies the problems of dealing with generic operations on different
types, as we have seen. Unfortunately, this is usually not the case.
Figure 2.26 illustrates a more complex arrangement of mixed types, this
one showing relations among different types of geometric figures. We see that,
in general, a type may have more than one subtype. Triangles and
quadrilaterals, for instance, are both subtypes of polygons. In addition, a
type may have more than one supertype. For example, an isosceles right
triangle may be regarded either as an isosceles triangle or as a right
triangle. This multiple-supertypes issue is particularly thorny, since it
means that there is no unique way to “raise” a type in the hierarchy.
Finding the “correct” supertype in which to apply an operation to an object
may involve considerable searching through the entire type network on the part
of a procedure such as apply-generic. Since there generally are
multiple subtypes for a type, there is a similar problem in coercing a value
“down” the type hierarchy. Dealing with large numbers of interrelated types
while still preserving modularity in the design of large systems is very
difficult, and is an area of much current research.
Figure 2.26: Relations among types of geometric figures.
如果系统中的数据类型能够自然地排列成一座塔,就会大大简化处理不同类型之间通用操作的问题,正如我们已经看到的那样。遗憾的是,这种情况通常并不成立。图 2.26 展示了混合类型之间一种更为复杂的安排,描述了不同种类几何图形之间的关系。可以看到,一般而言,一种类型可能有多个子类型。例如,三角形和四边形都是多边形的子类型。此外,一种类型也可能有多个超类型。例如,等腰直角三角形既可视为等腰三角形,也可视为直角三角形。多超类型问题尤为棘手,因为这意味着在层次体系中“提升”某一类型的方式并不唯一。为了找到“正确的”超类型以对某个对象施用某个操作,像 apply-generic 这样的过程可能需要对整个类型网络进行大量搜索。由于一种类型通常有多个子类型,在类型层次中向“下”强制转换某个值时,也会遇到类似的问题。在大型系统设计中,如何同时处理大量相互关联的类型并保持模块化,是一个极为困难的问题,也是当前研究的活跃领域。
(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)))) (define (scheme-number->complex n)
(make-complex-from-real-imag
(contents n) 0)) (put-coercion 'scheme-number 'complex
scheme-number->complex) (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)))))))