灯下 登录
计算机科学 / SICP / subsection 中英对照

2.4.3 Data-Directed Programming and Additivity

The general strategy of checking the type of a datum and calling an appropriate

procedure is called

dispatching on type. This is a powerful strategy

for obtaining modularity in system design. On the other hand, implementing the

dispatch as in 2.4.2 has two significant weaknesses. One

weakness is that the generic interface procedures (real-part,

imag-part, magnitude, and angle) must know about all the

different representations. For instance, suppose we wanted to incorporate a

new representation for complex numbers into our complex-number system. We

would need to identify this new representation with a type, and then add a

clause to each of the generic interface procedures to check for the new type

and apply the appropriate selector for that representation.

检查数据的类型并调用相应过程的通用策略称为按类型分派 (dispatching on type)。这是在系统设计中获得模块性的一种强有力的策略。然而,按照 2.4.2 节的方式实现分派存在两个明显的弱点。第一个弱点是,通用接口过程(`real-part`、`imag-part`、`magnitude` 和 `angle`)必须了解所有不同的表示。例如,假设我们想在复数系统中加入一种新的复数表示,就需要为这种新表示指定一个类型标签,然后在每个通用接口过程中增加一个条件子句,以检查新类型并为该表示调用适当的选择函数。

Another weakness of the technique is that even though the individual

representations can be designed separately, we must guarantee that no two

procedures in the entire system have the same name. This is why Ben and Alyssa

had to change the names of their original procedures from 2.4.1.

这种技术的另一个弱点是,尽管各种表示可以分别独立设计,但我们必须保证整个系统中没有两个过程同名。这正是 Ben 和 Alyssa 不得不将其在 2.4.1 节中定义的原始过程重新命名的原因。

The issue underlying both of these weaknesses is that the technique for

implementing generic interfaces is not

additive. The person

implementing the generic selector procedures must modify those procedures each

time a new representation is installed, and the people interfacing the

individual representations must modify their code to avoid name conflicts. In

each of these cases, the changes that must be made to the code are

straightforward, but they must be made nonetheless, and this is a source of

inconvenience and error. This is not much of a problem for the complex-number

system as it stands, but suppose there were not two but hundreds of different

representations for complex numbers. And suppose that there were many generic

selectors to be maintained in the abstract-data interface. Suppose, in fact,

that no one programmer knew all the interface procedures or all the

representations. The problem is real and must be addressed in such programs as

large-scale data-base-management systems.

这两个弱点的根本原因在于,实现通用接口的技术不具备可加性 (additive)。实现通用选择函数过程的人每次安装新表示时都必须修改那些过程,而实现各个具体表示的人也必须修改自己的代码以避免名字冲突。在这些情况下,需要做的修改虽然直截了当,但终究不得不做,这既带来不便又容易出错。对于目前规模的复数系统而言,这还不算大问题;但如果复数的表示不是两种而是数百种,情况将截然不同。再假设抽象数据接口中需要维护大量的通用选择函数,乃至没有任何一个程序员能够了解所有接口过程或所有表示,问题就变得真实而迫切,必须在大规模数据库管理系统这类程序中认真加以解决。

What we need is a means for modularizing the system design even further. This

is provided by the programming technique known as

data-directed programming.

To understand how data-directed programming works, begin with

the observation that whenever we deal with a set of generic operations that are

common to a set of different types we are, in effect, dealing with a

two-dimensional table that contains the possible operations on one axis and the

possible types on the other axis. The entries in the table are the procedures

that implement each operation for each type of argument presented. In the

complex-number system developed in the previous section, the correspondence

between operation name, data type, and actual procedure was spread out among

the various conditional clauses in the generic interface procedures. But the

same information could have been organized in a table, as shown in Figure 2.22.

Figure 2.22: Table of operations for the complex-number system.

我们需要一种能够进一步模块化系统设计的手段。数据导向编程 (data-directed programming) 这一编程技术正好提供了这样的手段。要理解数据导向编程的工作原理,先从以下观察入手:每当我们处理一组对多种不同类型都适用的通用操作时,实际上是在处理一张二维表——表的一个坐标轴是可能的操作,另一个坐标轴是可能的类型,表中的各项就是针对每种操作、每种参数类型而实现该操作的过程。在上一节开发的复数系统中,操作名称、数据类型与实际过程之间的对应关系分散在通用接口过程的各个条件子句中。但同样的信息完全可以组织成一张表,如图 2.22 所示。

图 2.22:复数系统的操作表。

Data-directed programming is the technique of designing programs to work with

such a table directly. Previously, we implemented the mechanism that

interfaces the complex-arithmetic code with the two representation packages as

a set of procedures that each perform an explicit dispatch on type. Here we

will implement the interface as a single procedure that looks up the

combination of the operation name and argument type in the table to find the

correct procedure to apply, and then applies it to the contents of the

argument. If we do this, then to add a new representation package to the

system we need not change any existing procedures; we need only add new entries

to the table.

数据导向编程就是让程序直接操作这样一张表的设计技术。此前,我们将复数算术代码与两个表示包衔接起来的机制是一组过程,每个过程都对类型执行显式分派。现在,我们改用单一的过程来实现这一接口:该过程在表中查找操作名与参数类型的组合,找到正确的过程后对参数的内容加以应用。这样一来,向系统中添加新的表示包时,无需修改任何已有的过程,只需向表中添加新条目即可。

To implement this plan, assume that we have two procedures, put and

get, for manipulating the operation-and-type table:

为实现这一方案,假设我们有两个过程 `put` 和 `get`,用于操作操作-类型表:

(put ⟨op⟩ ⟨type⟩ ⟨item⟩) installs the

⟨item⟩ in the table, indexed by the

⟨op⟩ and the ⟨type⟩.

`(put ⟨op⟩ ⟨type⟩ ⟨item⟩)` 将 `⟨item⟩` 安装到表中,以 `⟨op⟩` 和 `⟨type⟩` 为索引。

(get ⟨op⟩ ⟨type⟩) looks up the ⟨op⟩,

⟨type⟩ entry in the table and returns the item found there.

If no item is found, get returns false.

`(get ⟨op⟩ ⟨type⟩)` 在表中查找 `⟨op⟩`、`⟨type⟩` 对应的项并返回找到的内容。若未找到任何项,`get` 返回假。

For now, we can assume that put and get are included in our

language. In Chapter 3 (3.3.3) we

will see how to implement these and other operations for manipulating tables.

目前,我们可以假设 `put` 和 `get` 已内置于我们的语言中。在第 3 章(3.3.3 节)中,我们将看到如何实现这些以及其他用于操作表的过程。

Here is how data-directed programming can be used in the complex-number system.

Ben, who developed the rectangular representation, implements his code just as

he did originally. He defines a collection of procedures, or a

以下是数据导向编程在复数系统中的应用方式。开发了直角坐标表示的 Ben 完全按照他最初的方式实现自己的代码。他定义了一组过程,即一个

package, and interfaces these to the rest of the system by adding

entries to the table that tell the system how to operate on rectangular

numbers. This is accomplished by calling the following procedure:

包 (package),并通过向表中添加条目来将这个包与系统其余部分衔接,告知系统如何对直角坐标数进行操作。这是通过调用以下过程来完成的:

Notice that the internal procedures here are the same procedures from

2.4.1 that Ben wrote when he was working in isolation. No changes are

necessary in order to interface them to the rest of the system. Moreover,

since these procedure definitions are internal to the installation procedure,

Ben needn’t worry about name conflicts with other procedures outside the

rectangular package. To interface these to the rest of the system, Ben

installs his real-part procedure under the operation name

real-part and the type (rectangular), and similarly for the other

selectors. The interface also defines the

constructors to be used by the external system. These are identical to

Ben’s internally defined constructors, except that they attach the tag.

Alyssa’s polar package is analogous:

注意,这里的内部过程与 Ben 在 2.4.1 节独立工作时所写的过程完全相同,无需任何修改即可将它们与系统的其余部分衔接。此外,由于这些过程定义位于安装过程的内部,Ben 无需担心它们与直角坐标包之外的其他过程发生名字冲突。为了将这些过程与系统的其余部分衔接,Ben 以操作名 `real-part` 和类型 `(rectangular)` 为索引安装了他的 `real-part` 过程,其他选择函数类似处理。接口还定义了供外部系统使用的构造函数,它们与 Ben 内部定义的构造函数相同,区别仅在于附加了标签。Alyssa 的极坐标包与此类似:

Even though Ben and Alyssa both still use their original procedures defined

with the same names as each other’s (e.g., real-part), these definitions

are now internal to different procedures (see 1.1.8), so there is

no name conflict.

尽管 Ben 和 Alyssa 仍然使用各自定义的同名过程(例如 `real-part`),但这些定义现在位于不同的过程内部(参见 1.1.8 节),因此不存在名字冲突。

The complex-arithmetic selectors access the table by means of a general

“operation” procedure called apply-generic, which applies a generic

operation to some arguments. Apply-generic looks in the table under the

name of the operation and the types of the arguments and applies the resulting

procedure if one is present:

复数算术的选择函数通过一个名为 `apply-generic` 的通用“操作”过程来访问表,该过程将一个通用操作应用于若干参数。`apply-generic` 在表中以操作名和参数类型为索引进行查找,若找到相应的过程则加以应用:

Using apply-generic, we can define our generic selectors as follows:

利用 `apply-generic`,我们可以如下定义通用选择函数:

Observe that these do not change at all if a new representation is added to the

system.

请注意,当系统中加入新的表示时,这些过程完全不需要改动。

We can also extract from the table the constructors to be used by the programs

external to the packages in making complex numbers from real and imaginary

parts and from magnitudes and angles. As in 2.4.2, we construct

rectangular numbers whenever we have real and imaginary parts, and polar

numbers whenever we have magnitudes and angles:

我们还可以从表中提取构造函数,供包外部的程序用实部与虚部或模与辐角来构造复数。与 2.4.2 节一样,当已知实部和虚部时构造直角坐标数,当已知模和辐角时构造极坐标数:

Racket #lang sicp
(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)
Racket #lang sicp
(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)
Racket #lang sicp
(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))))))
Racket #lang sicp
(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))
Racket #lang sicp
(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))