One way to view data abstraction is as an application of the “principle of
least commitment.” In implementing the complex-number system in
2.4.1, we can use either Ben’s rectangular representation or Alyssa’s
polar representation. The abstraction barrier formed by the selectors and
constructors permits us to defer to the last possible moment the choice of a
concrete representation for our data objects and thus retain maximum
flexibility in our system design.
看待数据抽象的一种方式,是将其视为"最少承诺原则"(principle of least commitment) 的一种运用。在 2.4.1 节实现复数系统时,我们既可以使用 Ben 的直角坐标表示,也可以使用 Alyssa 的极坐标表示。由选择函数和构造函数构成的抽象屏障,使我们能够将为数据对象选择具体表示方式的决定推迟到最后一刻,从而在系统设计中保持最大的灵活性。
The principle of least commitment can be carried to even further extremes. If
we desire, we can maintain the ambiguity of representation even after we
have designed the selectors and constructors, and elect to use both Ben’s
representation and Alyssa’s representation. If both representations are
included in a single system, however, we will need some way to distinguish data
in polar form from data in rectangular form. Otherwise, if we were asked, for
instance, to find the magnitude of the pair (3, 4), we wouldn’t know
whether to answer 5 (interpreting the number in rectangular form) or 3
(interpreting the number in polar form). A straightforward way to accomplish
this distinction is to include a
type tag—the symbol
rectangular or polar—as part of each complex number. Then
when we need to manipulate a complex number we can use the tag to decide which
selector to apply.
最少承诺原则还可以被推进得更为彻底。如果我们愿意,可以在设计好选择函数和构造函数之后仍然保持表示方式的不确定性,并决定同时使用 Ben 的表示方式和 Alyssa 的表示方式。然而,如果在一个系统中同时包含两种表示方式,我们就需要某种手段来区分极坐标形式的数据和直角坐标形式的数据。否则,当有人要求我们求序对 (3, 4) 的模时,我们将无从判断答案应该是 5(将其解释为直角坐标形式)还是 3(将其解释为极坐标形式)。实现这种区分的一种直接方式,是将一个类型标签 (type tag)——符号 rectangular 或 polar——作为每个复数的组成部分。这样,当我们需要操作一个复数时,便可以用这个标签来决定应调用哪个选择函数。
In order to manipulate tagged data, we will assume that we have procedures
type-tag and contents that extract from a data object the tag and
the actual contents (the polar or rectangular coordinates, in the case of a
complex number). We will also postulate a procedure attach-tag that
takes a tag and contents and produces a tagged data object. A straightforward
way to implement this is to use ordinary list structure:
为了操作带标签数据 (tagged data),我们假设有过程 type-tag 和 contents,它们分别从数据对象中提取标签和实际内容(对于复数来说,即极坐标或直角坐标)。我们还假设有一个过程 attach-tag,它接受一个标签和内容,并产生一个带标签的数据对象。实现这一点的直接方式是使用普通的表结构:
Using these procedures, we can define predicates rectangular? and
polar?, which recognize rectangular and polar numbers, respectively:
利用这些过程,我们可以定义谓词 rectangular? 和 polar?,它们分别用于识别直角坐标数和极坐标数:
With type tags, Ben and Alyssa can now modify their code so that their two
different representations can coexist in the same system. Whenever Ben
constructs a complex number, he tags it as rectangular. Whenever Alyssa
constructs a complex number, she tags it as polar. In addition, Ben and Alyssa
must make sure that the names of their procedures do not conflict. One way to
do this is for Ben to append the suffix rectangular to the name of each
of his representation procedures and for Alyssa to append polar to the
names of hers. Here is Ben’s revised rectangular representation from
2.4.1:
有了类型标签,Ben 和 Alyssa 现在可以修改各自的代码,使两种不同的表示方式能够在同一个系统中共存。每当 Ben 构造一个复数时,他将其标记为 rectangular;每当 Alyssa 构造一个复数时,她将其标记为 polar。此外,Ben 和 Alyssa 还必须确保各自的过程名不发生冲突。一种做法是:Ben 在其每个表示过程的名称后面加上后缀 rectangular,Alyssa 则在她的过程名称后面加上 polar。以下是 Ben 对 2.4.1 节中直角坐标表示的修订版本:
and here is Alyssa’s revised polar representation:
以下是 Alyssa 修订后的极坐标表示:
Each generic selector is implemented as a procedure that checks the tag of its
argument and calls the appropriate procedure for handling data of that type.
For example, to obtain the real part of a complex number, real-part
examines the tag to determine whether to use Ben’s real-part-rectangular
or Alyssa’s real-part-polar. In either case, we use contents to
extract the bare, untagged datum and send this to the rectangular or polar
procedure as required:
每个通用选择函数都被实现为一个过程,该过程检查其参数的标签,然后调用适当的过程来处理对应类型的数据。例如,为了取得复数的实部,real-part 检查标签以决定是使用 Ben 的 real-part-rectangular 还是 Alyssa 的 real-part-polar。无论哪种情况,都用 contents 提取出裸的、不带标签的数据,再将其传递给直角坐标或极坐标过程:
To implement the complex-number arithmetic operations, we can use the same
procedures add-complex, sub-complex, mul-complex, and
div-complex from 2.4.1, because the selectors they call
are generic, and so will work with either representation. For example, the
procedure add-complex is still
为了实现复数的算术运算,我们可以沿用 2.4.1 节中的 add-complex、sub-complex、mul-complex 和 div-complex,因为它们所调用的选择函数都是通用的,因而可以与任一种表示方式配合使用。例如,过程 add-complex 仍然是
Finally, we must choose whether to construct complex numbers using Ben’s
representation or Alyssa’s representation. One reasonable choice is to
construct rectangular numbers whenever we have real and imaginary parts and to
construct polar numbers whenever we have magnitudes and angles:
最后,我们必须决定是用 Ben 的表示方式还是 Alyssa 的表示方式来构造复数。一个合理的选择是:当已知实部和虚部时构造直角坐标数,当已知模和辐角时构造极坐标数:
The resulting complex-number system has the structure shown in Figure 2.21.
The system has been decomposed into three relatively independent parts:
the complex-number-arithmetic operations, Alyssa’s polar implementation, and
Ben’s rectangular implementation. The polar and rectangular implementations
could have been written by Ben and Alyssa working separately, and both of these
can be used as underlying representations by a third programmer implementing
the complex-arithmetic procedures in terms of the abstract constructor/selector
interface.
Figure 2.21: Structure of the generic complex-arithmetic system.
由此得到的复数系统具有图 2.21 所示的结构。整个系统被分解为三个相对独立的部分:复数算术运算、Alyssa 的极坐标实现,以及 Ben 的直角坐标实现。极坐标实现和直角坐标实现可以由 Ben 和 Alyssa 分别独立完成,而第三位程序员则可以在抽象的构造函数/选择函数接口之上实现复数算术过程,并以前两者作为底层表示。
图 2.21:通用复数算术系统的结构。
Since each data object is tagged with its type, the selectors operate on the
data in a generic manner. That is, each selector is defined to have a behavior
that depends upon the particular type of data it is applied to. Notice the
general mechanism for interfacing the separate representations: Within a given
representation implementation (say, Alyssa’s polar package) a complex number is
an untyped pair (magnitude, angle). When a generic selector operates on a
number of polar type, it strips off the tag and passes the contents on
to Alyssa’s code. Conversely, when Alyssa constructs a number for general use,
she tags it with a type so that it can be appropriately recognized by the
higher-level procedures. This discipline of stripping off and attaching tags
as data objects are passed from level to level can be an important
organizational strategy, as we shall see in 2.5.
由于每个数据对象都带有其类型的标签,选择函数能够以通用的方式操作数据。也就是说,每个选择函数的行为都取决于所作用数据的具体类型。请注意这种衔接各独立表示方式的通用机制:在给定的表示方式实现内部(例如 Alyssa 的极坐标程序包),复数是一个不带类型的序对(模,辐角)。当通用选择函数作用于极坐标类型的数时,它会剥去标签,并将内容传递给 Alyssa 的代码。反之,当 Alyssa 为通用目的构造一个数时,她会为其附上类型标签,以便上层过程能够正确识别它。这种在数据对象从一层传递到另一层时剥去再附上标签的规范做法,可以成为一种重要的组织策略,正如我们将在 2.5 节中看到的那样。
(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)) (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))))) (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))) (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))))
(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)))) (define (add-complex z1 z2)
(make-from-real-imag
(+ (real-part z1) (real-part z2))
(+ (imag-part z1) (imag-part z2)))) (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))