# 2.4.2标记数据

> 译者： [https://sicp.comp.nus.edu.sg/chapters/41](https://sicp.comp.nus.edu.sg/chapters/41)

一种查看数据抽象的方法是应用&lt;quote&gt;最小承诺原则。&lt;/quote&gt; 在实现 [2.4.1](40) 部分中的复数系统时，我们可以使用Ben的矩形表示形式或Alyssa的极坐标表示形式。 选择器和构造函数形成的抽象障碍使我们可以将为数据对象选择具体表示的选择推迟到最后可能的时刻，从而在我们的系统设计中保留最大的灵活性。

最小承诺原则可以进一步延伸到极端。 如果需要，即使在设计了选择器和构造函数并选择使用Ben的表示形式_和_ Alyssa表示形式之后，甚至在设计之后，我们仍然可以保持表示形式的歧义。 但是，如果两种表示形式都包含在一个系统中，我们将需要某种方式来区分极性形式的数据和矩形形式的数据。 否则，例如，如果要求我们找到对![%283%2C4%29](img/850125459bb36f865a35977a34eb2923.jpg)中的`magnitude`，我们将不知道是回答5（以矩形形式解释数字）还是回答3（以极性形式解释数字） 。 实现此区别的一种直接方法是在每个复数中包含_类型标签_（符号`rectangular`或`polar`）。 然后，当我们需要操纵一个复数时，我们可以使用标签来决定应用哪个选择器。

为了处理标记的数据，我们将假定我们具有函数`type_tag`和`contents`，它们从数据对象中提取标签和实际内容（在复数的情况下为极坐标或直角坐标）。 我们还将假定一个函数`attach_tag`，该函数带有标签和内容，并生成带标签的数据对象。 一种简单的实现方法是使用普通列表结构：

```js
function attach_tag(type_tag, contents) {
    return pair(type_tag, contents);
}
function type_tag(datum) {
    return is_pair(datum)
           ? head(datum)
           : Error("bad tagged datum in type_tag", datum);
}
function contents(datum) {
    return is_pair(datum)
           ? tail(datum)
           : Error("bad tagged datum in contents", datum);
}
```

使用这些函数，我们可以定义谓词`is_rectangular`和`is_polar`，它们分别识别极数和矩形数：

```js
function is_rectangular(z) {
    return type_tag(z) === "rectangular";
}
function is_polar(z) {
    return type_tag(z) === "polar";
}
```

使用类型标签，Ben和Alyssa现在可以修改其代码，以便它们的两种不同表示形式可以共存于同一系统中。 每当Ben构造一个复数时，他都会将其标记为矩形。 每当Alyssa构造复数时，她都将其标记为Polar。 此外，Ben和Alyssa必须确保其职能的名称不会冲突。 一种方法是让Ben将后缀`rectangular`附加到其每个表示函数的名称上，并让Alyssa将`polar`附加到其表示的名称上。 这是Ben在 [2.4.1](40) 部分中修改后的矩形表示：

```js
function real_part_rectangular(z) {
    return head(z);
}
function imag_part_rectangular(z) {
    return tail(z);
}
function magnitude_rectangular(z) {
    return math_sqrt(square(real_part_rectangular(z))
           +
           square(imag_part_rectangular(z)));
}
function angle_rectangular(z) {
    return math_atan(imag_part_rectangular(z),
                real_part_rectangular(z));
}
function make_from_real_imag_rectangular(x, y) {
    return attach_tag("rectangular",
                      pair(x, y));
}
function make_from_mag_ang_rectangular(r, a) {
    return attach_tag("rectangular",
                      pair(r * math_cos(a), r * math_sin(a)));
}
```

and here is Alyssa's revised polar representation:

```js
function real_part_polar(z) {
    return magnitude_polar(z) * math_cos(angle_polar(z));
}
function imag_part_polar(z) {
    return magnitude_polar(z) * math_sin(angle_polar(z));
}
function magnitude_polar(z) {
    return head(z);
}
function angle_polar(z) {
    return tail(z);
}
function make_from_real_imag_polar(x, y) {
    return attach_tag("polar",
                     pair(math_sqrt(square(x) + square(y)),
                          math_atan(y, z)));
}
function make_from_mag_ang_polar(r, a) {
    return attach_tag("polar",
                      pair(r, a));
}
```

Each generic selector is implemented as a function that checks the tag of its argument and calls the appropriate function 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 function as required:

```js
function real_part(z) {
    return is_rectangular(z)
           ? real_part_rectangular(contents(z))
           : is_polar(z)
             ? real_part_polar(contents(z))
             : Error("Unknown type in real_part", z);
}
function imag_part(z) {
    return is_rectangular(z)
           ? imag_part_rectangular(contents(z))
           : is_polar(z)
             ? imag_part_polar(contents(z))
             : Error("Unknown type in imag_part", z);
}
function magnitude(z) {
    return is_rectangular(z)
           ? magnitude_rectangular(contents(z))
           : is_polar(z)
             ? magnitude_polar(contents(z))
             : Error("Unknown type in magnitude", z);
}
function angle(z) {
    return is_rectangular(z)
           ? angle_rectangular(contents(z))
           : is_polar(z)
             ? angle_polar(contents(z))
             : Error("Unknown type in angle", z);
}
```

要实现复数算术运算，我们可以使用 [2.4.1](40) 部分中的相同函数`add_complex`，`sub_complex`，`mul_complex`和`div_complex`，因为 他们调用的选择器是通用的，因此可以使用任一种表示形式。 例如，功能`add_complex`仍然

```js
function add_complex(z1, z2) {
   return make_from_real_imag(
              real_part(z1) + real_part(z2),
              imag_part(z1) + imag_part(z2));
}
```

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:

```js
function make_from_real_imag(x, y) {
    return make_from_real_imag_rectangular(x, y);
}
function make_from_mag_ang(r, a) {
    return make_from_mag_ang_polar(r, a);
}
```

<figure>![](img/5aa777ea29a5fbbef118174e662a316d.jpg)**[图2.21](41#fig_2.21)** 通用复数算法系统的结构。</figure>

得到的复数系统具有如图 <ref name="fig:generic-complex-system">[2.21](41#fig_2.21)</ref> 所示的结构。 该系统已分解为三个相对独立的部分：复数算术运算，Alyssa的Polar实现和Ben的矩形实现。 极坐标和矩形实现可能是由Ben和Alyssa分别编写的，并且它们都可以由在抽象构造函数/选择器接口方面实现复杂算术功能的第三位程序员用作基础表示。

由于每个数据对象都用其类型标记，因此选择器以通用方式对数据进行操作。 也就是说，每个选择器都被定义为具有一种行为，该行为取决于所应用的特定数据类型。 请注意连接单独表示的通用机制：在给定表示实现中（例如，Alyssa的Polar包），复数是未键入的对（大小，角度）。 当通用选择器对多种`polar`类型进行操作时，它会剥离标签并将内容传递给Alyssa的代码。 相反，当Alyssa构造一个通用数字时，她用类型标记它，以便更高级别的函数可以适当地识别它。 正如我们将在 [2.5](43) 部分中看到的那样，在数据对象从一个级别传递到另一个级别时，剥离和附加标签的原则可能是一项重要的组织策略。