# 1.1.8充当黑盒抽象

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

函数`sqrt`是我们通过一组相互定义的函数定义的过程的第一个示例。 注意，`sqrt_iter`的声明是_递归_； 也就是说，功能是根据自身定义的。 能够根据自身定义功能的想法可能令人不安； 似乎尚不清楚这样的&lt;quote&gt;通告&lt;/quote&gt;定义是如何有意义的，更不用说指定由计算机执行的明确定义的过程了。 这将在 [1.2](11) 部分中更仔细地解决。 但是首先让我们考虑一下`sqrt`示例所说明的其他一些要点。

可以发现，计算平方根的问题自然分解为许多子问题：如何判断猜测是否足够好，如何改善猜测，等等。 这些任务中的每一个都是通过单独的功能完成的。 整个`sqrt`程序可以看作一个函数簇（如图 [1.2](10#fig_1.2) 所示），将问题的分解反映为子问题。

 <split><figure>![](img/43b7fd7493c947f1ade4af21eabae357.jpg)**[Figure 1.2](10#fig_1.2)** Functional decomposition of the `sqrt` program.</figure></split> 

这种分解策略的重要性不仅在于将程序划分为多个部分。 毕竟，我们可以将任何大型程序都分成几部分-前十行，后十行，后十行，依此类推。 相反，至关重要的是每个功能都必须完成可识别的任务，该任务可以用作定义其他功能的模块。 例如，当我们根据`square`定义`good_enough`函数时，我们可以将`square`函数视为&lt;quote&gt;黑盒。&lt;/quote&gt; 那时我们不关心_函数如何计算_，仅考虑_它计算平方的事实。 可以抑制如何计算平方的细节，以便稍后考虑。 实际上，就`good_enough`功能而言，`square`并不是一个功能，而是一个功能的抽象，即所谓的_功能抽象_。 在此抽象级别上，任何计算平方的函数都一样好。_

因此，仅考虑它们返回的值，以下两个对数字求平方的函数应该是不可区分的。 每个参数都有一个数字参数，并产生该数字的平方作为值。 [[1]](10#footnote-1)

```js
function square(x) {
    return x * x;
}
```

```js
function square(x) {
    return math_exp(double(math_log(x)));
}
function double(x) {
    return x + x;
}
```

因此，一个功能应该能够抑制细节。 函数的用户可能没有自己编写函数，但是可能已从其他程序员那里获得了黑匣子。 用户无需为了使用它就知道如何实现该功能。

## 本地名称

函数实现的一个细节对函数的用户来说应该无关紧要，是实现者为函数的参数选择名称。 因此，以下功能不应区分开：

```js
function square(x) {
    return x * x;
}
```

```js
function square(y) {
    return y * y;
}
```

从表面上看，这个原则（函数的含义应独立于函数作者使用的参数名称）似乎是不言而喻的，但是其后果却是深远的。 最简单的结果是，函数的参数名称必须位于函数主体的本地。 例如，我们在平方根函数的`good_enough`声明中使用了`square`：

```js
function good_enough(guess, x) {
    return abs(square(guess) - x) < 0.001;
}
```

`good_enough`的作者的目的是确定第一个参数的平方是否在第二个参数的给定公差内。 我们看到`good_enough`的作者使用名称`guess`来引用第一个参数，而`x`则使用了第二个参数。 `square`的参数为`guess`。 如果`square`的作者使用`x`（如上所述）来引用该论点，我们将看到`good_enough`中的`x`必须与`square`中的不同。 运行函数`square`一定不会影响`good_enough`使用的`x`的值，因为`square`完成计算后，`good_enough`可能需要`x`的值。

如果参数不是它们各自功能主体的局部参数，则可以将`square`中的参数`x`与`good_enough`中的参数`x`混淆，并且`good_enough`的行为取决于哪个版本 我们使用了`square`。 因此，`square`不会是我们想要的黑匣子。

函数的参数在函数声明中具有非常特殊的作用，因为参数的名称无关紧要。 这样的名称称为_绑定_，我们说函数声明_绑定_它的参数。 如果绑定名称在整个声明中始终被重命名，则函数声明的含义将保持不变。 [[2]](10#footnote-2) 如果未绑定名称，则说它没有_。 绑定为其声明名称的一组表达式称为该名称的_作用域_。 在函数声明中，声明为函数参数的绑定名称将函数主体作为其作用域。_

 <split>在上面的`good_enough`声明中，`guess`和`x`是绑定名称，但是`abs`和`square`是自由的。 `good_enough`的含义应独立于我们为`guess`和`x`选择的名称，只要它们与`abs`和`square`不同。 （如果将`guess`重命名为`abs`，我们将通过_捕获_这个名称`abs`引入一个错误。它将从自由更改为绑定。）`good_enough`的含义不是独立的 但是，可以选择其免费名称。 当然，它取决于此事实（此声明的外部），符号`abs`命名了一个用于计算数字绝对值的函数。 如果我们在声明中将`abs`替换为`abs`（JavaScript的余弦函数），则JavaScript函数`good_enough`将计算不同的数学函数。</split> 

## 内部声明和块结构

到目前为止，我们可以使用一种名称隔离：函数的参数对于函数主体而言是局部的。 平方根程序说明了我们希望控制名称使用的另一种方法。 现有程序包含单独的功能：

```js
function sqrt(x) {
    return sqrt_iter(1.0, x);
}
function sqrt_iter(guess, x) {
    return good_enough(guess, x)
           ? guess
           : sqrt_iter(improve(guess, x), x);
}
function good_enough(guess, x) {
    return abs(square(guess) - x) < 0.001;
}
function improve(guess, x) {
    return average(guess, x / guess);
}
```

该程序的问题在于，对`sqrt`的用户来说唯一重要的功能是`sqrt`。 其他功能（`sqrt_iter`，`good_enough`和 &lt;nobr&gt;`improve`）&lt;/nobr&gt;只会使他们头脑混乱。 他们可能不会将称为`good_enough`的任何其他函数声明为与平方根程序一起工作的另一个程序的一部分，因为`sqrt`需要它。 在许多独立的程序员构建大型系统时，这个问题尤其严重。 例如，在大型数值函数库的构造中，许多数值函数被计算为逐次逼近，因此可能具有名为`good_enough`和`improve`的函数作为辅助函数。 我们想定位子功能，将它们隐藏在`sqrt`中，以便`sqrt`可以与其他连续逼近并存，每个逼近都有各自的专用`good_enough`功能。 为了使之成为可能，我们允许一个函数具有该函数本地的内部声明。 例如，在平方根问题中，我们可以写

```js
function sqrt(x) {
    function good_enough(guess, x) {
        return abs(square(guess) - x) < 0.001;
    }
    function improve(guess, x) {
        return average(guess, x / guess);
    }
    function sqrt_iter(guess, x) {
        return good_enough(guess, x) 
                   ? guess
                   : sqrt_iter(improve(guess, x), x);
    }
   return sqrt_iter(1.0, x);
}
```

函数的主体（用大括号括起来的语句）称为_块_。 嵌套在块内的函数声明是该块的局部声明。 _块结构_基本上是最简单的名称打包问题的正确解决方案。 但是这里有一个更好的主意。 除了内部化辅助函数的声明之外，我们还可以简化它们。 由于`x`绑定在`sqrt`的声明中，因此`sqrt`内部定义的函数`good_enough`，`improve`和`sqrt_iter`属于`x`的范围。 因此，不必将`x`明确传递给这些功能中的每一个。 相反，我们允许`x`在内部声明中为自由名称，如下所示。 然后`x`从调用封闭函数`sqrt`的参数中获取其值。 该学科称为_词法作用域_。 [[3]](10#footnote-3)

```js
function sqrt(x) {
    function good_enough(guess) {
        return abs(square(guess) - x) < 0.001;
    }
    function improve(guess) {
        return average(guess, x / guess);
    }
    function sqrt_iter(guess) {
        return good_enough(guess)
               ? guess
               : sqrt_iter(improve(guess));
   }
   return sqrt_iter(1.0);
}
```

我们将广泛使用块结构来帮助我们将大型程序分解为易于处理的部分。 [[4]](10#footnote-4) 块结构的思想起源于编程语言Algol 60，它出现在大多数高级编程语言中，并且是帮助组织大型程序构建的重要工具。

* * *

[[1]](10#footnote-link-1) It is not even clear which of these functions is a more efficient implementation. This depends upon the hardware available. There are machines for which the <quote>obvious</quote> implementation is the less efficient one. Consider a machine that has extensive tables of logarithms and antilogarithms stored in a very efficient manner.

[[2]](10#footnote-link-2) The concept of consistent renaming is actually subtle and difficult to define formally. Famous logicians have made embarrassing errors here.

[[3]](10#footnote-link-3) Lexical scoping dictates that free names in a function are taken to refer to bindings made by enclosing function declarations; that is, they are looked up in the environment in which the function was declared. We will see how this works in detail in chapter 3 when we study environments and the detailed behavior of the interpreter.

[[4]](10#footnote-link-4) Embedded declarations must come first in a function body. The management is not responsible for the consequences of running programs that intertwine declaration and use.

