# 2.1.1示例：有理数的算术运算

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

假设我们要对有理数进行算术运算。 我们希望能够对它们进行加，减，乘和除，并测试两个有理数是否相等。

让我们首先假设我们已经有一种从分子和分母构造有理数的方法。 我们还假定，在给定一个有理数的情况下，我们有一种提取（或选择）其分子和分母的方法。 让我们进一步假设构造函数和选择器可以作为函数使用：

*   `make_rat(n, d)`返回分子为整数`n`且分母为整数`d`的有理数。
*   `numer(x)`返回有理数`x`的分子。
*   `denom(x)`返回有理数`x`的分母。

我们在这里使用一种强大的综合策略：_一厢情愿_。 我们还没有说过如何表示有理数，或者应该如何实现函数`numer`，`denom`和`make_rat`。 即便如此，如果我们确实具有这三个功能，则可以使用以下关系来加，减，乘，除和测试相等性：![%0A%20%20%20%20%20%20%5Cfrac%7Bn_%7B1%7D%7D%7Bd_%7B1%7D%7D%2B%5Cfrac%7Bn_%7B2%7D%7D%7Bd_%7B2%7D%7D%3D%5Cfrac%7Bn_%7B1%7Dd_%7B2%7D%2Bn_%7B2%7Dd_%7B1%7D%7D%7Bd_%7B1%7Dd_%7B2%7D%7D%0A%20%20%20%20%20%20](img/03acc46e5f3825994621e54cb1a0c51d.jpg) ![%0A%20%20%20%20%20%20%5Cfrac%7Bn_%7B1%7D%7D%7Bd_%7B1%7D%7D-%5Cfrac%7Bn_%7B2%7D%7D%7Bd_%7B2%7D%7D%3D%5Cfrac%7Bn_%7B1%7Dd_%7B2%7D-n_%7B2%7Dd_%7B1%7D%7D%7Bd_%7B1%7Dd_%7B2%7D%7D%0A%20%20%20%20%20%20](img/f4e9341794eb6a4b1cd28d6c800dea64.jpg) ![%0A%20%20%20%20%20%20%5Cfrac%7Bn_%7B1%7D%7D%7Bd_%7B1%7D%7D%5Ccdot%5Cfrac%7Bn_%7B2%7D%7D%7Bd_%7B2%7D%7D%3D%5Cfrac%7Bn_%7B1%7Dn_%7B2%7D%7D%7Bd_%7B1%7Dd_%7B2%7D%7D%0A%20%20%20%20%20%20](img/78de45adac8bb9e7f578ad07312929b8.jpg) ![%0A%20%20%20%20%20%20%5Cfrac%7Bn_%7B1%7D%2Fd_%7B1%7D%7D%7Bn_%7B2%7D%2Fd_%7B2%7D%7D%3D%5Cfrac%7Bn_%7B1%7Dd_%7B2%7D%7D%7Bd_%7B1%7Dn_%7B2%7D%7D%0A%20%20%20%20%20%20](img/fb57ae78d00da6a1c21d04cbda6dce77.jpg) ![%20%0A%20%20%20%20%20%20%5Cfrac%7Bn_%7B1%7D%7D%7Bd_%7B1%7D%7D%3D%5Cfrac%7Bn_%7B2%7D%7D%7Bd_%7B2%7D%7D%5C%20%5Ctextrm%7Bif%20%20and%20only%20if%7D%20%5C%20%20n_%7B1%7Dd_%7B2%7D%3Dn_%7B2%7Dd_%7B1%7D%0A%20%20%20%20%20%20](img/6b36e5b92642df0adf87f800f88f77de.jpg)

我们可以将这些规则表示为函数：

```js
function add_rat(x, y) {
    return make_rat(numer(x) * denom(y) + numer(y) * denom(x),
                    denom(x) * denom(y));
}
function sub_rat(x, y) {
    return make_rat(numer(x) * denom(y) - numer(y) * denom(x),
                    denom(x) * denom(y));
}
function mul_rat(x, y) {
    return make_rat(numer(x) * numer(y),
                    denom(x) * denom(y));
}
function div_rat(x, y) {
    return make_rat(numer(x) * denom(y),
                    denom(x) * numer(y));
}
function equal_rat(x, y) {
    return numer(x) * denom(y) === numer(y) * denom(x);
}
```

现在，我们对根据选择器和构造函数`numer`，`denom`和`make_rat`定义的有理数进行了运算。 但是我们还没有定义这些。 我们需要某种方式将分子和分母粘合在一起以形成有理数。

## 对

为了使我们能够实现数据抽象的具体级别，我们的语言提供了一种称为_对_的复合结构，可以使用函数`pair`对其进行构造。 此函数接受两个参数，并返回一个包含两个参数作为部分的复合数据对象。 给定一对，我们可以使用`head`和`tail`函数提取零件。 这些函数不是JavaScript中的&lt;quote&gt;原语&lt;/quote&gt;函数，但本书中的程序将它们视为正确。 [[1]](25#footnote-1) 因此，我们可以如下使用`pair`，`head`和`tail`：

```js
const x = pair(1,2);
```

```js
head(x);
```

```js
tail(x);
```

请注意，对是一个数据对象，可以像原始数据对象一样被赋予名称并可以对其进行操作。 此外，`pair`可用于形成其元素为对的对，依此类推：

```js
const x = pair(1,2);
const y = pair(3,4);
const z = pair(x,y);
```

```js
head(head(z));
```

```js
head(tail(z));
```

在 [2.2](29) 部分中，我们将看到这种组合对的能力如何意味着对可以用作通用构造块来创建各种复杂的数据结构。 由功能`pair`，`head`和`tail`实现的单个复合数据原语_对_是我们唯一需要的胶水。 从成对构造的数据对象称为_列表结构的_数据。

## 表示有理数

对提供了一种完善有理数系统的自然方法。 简单地将有理数表示为两个整数对：分子和分母。 然后，`make_rat`，`numer`和`denom`易于实现如下： [[2]](25#footnote-2)

```js
function make_rat(n, d) {
    return pair(n, d);
}
function numer(x) {
    return head(x);
}
function denom(x) {
    return tail(x);
}
```

另外，为了显示我们的计算结果，我们可以通过打印分子，斜杠和&lt;split&gt;分母来打印有理数： [[3]](25#footnote-3)&lt;/split&gt;

```js
function print_rat(x) {
    display(numer(x));
    display("-");
    display(denom(x));
}
```

现在我们可以尝试我们的有理数函数：

```js
const one_half = make_rat(1, 2);

print_rat(one_half);
```

```js
const one_third = make_rat(1, 3);

print_rat(one_third);
```

```js
print_rat(add_rat(one_half, one_third));
```

```js
print_rat(mul_rat(one_half, one_third));
```

```js
print_rat(div_rat(one_half, one_third));
```

如最后一个示例所示，我们的有理数实现不会将有理数减少到最低的条件。 我们可以通过更改`make_rat`对此进行补救。 如果我们具有`gcd`函数，如 [1.2.5](16) 一节中的函数，该函数产生两个整数的最大公约数，则可以使用`gcd`来减小分子和 分母建立对之前的最低项：

```js
function make_rat(n, d) {
    const g = gcd(n, d);
    return pair(n / g, d / g);
}
```

现在我们有

```js
function numer(x) {
    return head(x);
}
function denom(x) {
    return tail(x);
}
function add_rat(x, y) {
    return make_rat(numer(x) * denom(y) + numer(y) * denom(x),
                    denom(x) * denom(y));
}
function sub_rat(x, y) {
    return make_rat(numer(x) * denom(y) - numer(y) * denom(x),
                    denom(x) * denom(y));
}
function mul_rat(x, y) {
    return make_rat(numer(x) * numer(y),
                    denom(x) * denom(y));
}
function div_rat(x, y) {
    return make_rat(numer(x) * denom(y),
                    denom(x) * numer(y));
}
function equal_rat(x, y) {
    return numer(x) * denom(y) === numer(y) * denom(x);
}
```

```js
print_rat(add_rat(one_third, one_third));
```

as desired. This modification was accomplished by changing the constructor `make_rat` without changing any of the functions (such as `add_rat` and `mul_rat`) that implement the actual operations.<exercise>Define a better version of `make_rat` that handles both positive and negative arguments. The function `make_rat` should normalize the sign so that if the rational number is positive, both the numerator and denominator are positive, and if the rational number is negative, only the numerator is negative.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_25_1_div">Solution</button> <solution>```js
function sign(x) {
    return x < 0 
           ? -1
	   : x > 0
	     ? 1
	     : 0;
}
function make_rat(n, d) {
    const g = gcd(n, d);
    return pair(sign(n) * sign(d) * abs(n / g), 
                abs(d / g));
}
```</solution></exercise> 

* * *

[[1]](25#footnote-link-1) In practice, we provide these functions in a _library_ and make sure that the library is loaded whenever we need them. One way of defining these functions is to use _arrays_, one of JavaScript's primitive data structures.

[[2]](25#footnote-link-2) Another way to define the selectors and constructor is

```js
const make_rat = pair;
const numer = head;
const denom = tail;
```

The first definition associates the name `make_rat` with the value of the expression `pair`, which is the primitive function that constructs pairs. Thus `make_rat` and `pair` are names for the same primitive constructor.

以这种方式定义选择器和构造函数非常有效：`make_rat`并不是`make_rat` _调用_ `pair`，而是 `pair`，因此只调用了一个函数，而不是两个 ，当调用`make_rat`时。 另一方面，这样做会破坏跟踪辅助函数调用或在函数调用上设置断点的调试辅助工具：您可能希望观看`make_rat`被调用，但是您当然不希望观看对`pair`的每次调用。

我们选择在本书中不要使用这种定义方式。

 [[3]](25#footnote-link-3) The primitive function `display` is our facility for printing data. We display a rational number by printing its denominator in one line, a dash in the next line and its numerator in the last line.

