1.1.8 作为黑盒抽象的程序

求平方根的程序是程序执行过程的定义由一系列相互有关联的程序定义组成的第一个例子。
注意到sqrt-iter的定义是递归的。也就是说,程序定义使用了本身。程序递归定义的想法
可能是让人有困扰的。 如此一个环形的定义使用计算机来执行的话,它更少的指定出一个
定义良好的执行流程。在1.2部分中,这个问题被更好的处理。 但是现在首先让我们考虑
由sqrt例子显示出的一些其它的重要的问题点。

要注意到的问题是,求平方根的问题,被分解成一系列的子问题。怎么判断一个猜测的值是否
足够好,怎么改进一个猜测的值。等等。这些任务都由单独的程序来完成。
求平方根的程序被显示为一系列的程序组成的。见图1.2.它显示出问题被分解成子问题。

           sqrt
            |
            |
          sqrt-iter
         /         \
        /           \
       /             \
     good-enough    improve
           / \           |
          /   \          |
         /     \         |
     square   abs      average
     图1.2 程序sqrt的程序性分解

这种分解策略的重要性在于,它不是简单地把程序分成各个部分。毕竟我们能把任何一个大的程序,
分成各个部分,如第一个十行,第二个十行,下一个十行等等。而是任何一个程序都能完成一个
单独的任务,这种程序能在定义其它的程序中,作为一个模块被使用。这才是关键的。
例如,当我们在square程序中,定义程序 good-enough?这个程序,我们能够把square程序作为一个黑盒。
我们不用关注程序怎么计算结果 ,仅关注它能计算平方的事实。平方如何计算的细节能被隐藏起来,
在稍后的时候再被考虑。实际上,正如good-enough?程序关注的,square不仅是一个程序,也是一个
程序的抽象。叫做程序化的抽象。在这个抽象的层面上,任何一个程序都能计算平方是足够得好。

因此,考虑它们的返回值,如下的这两个程序都计算一个数的平方,它们应该是没有区别的。它们都是有一个
数作为参数,生成这个数的平方值作为返回值。

(define (square x) (* x x))

(define (square x)
   (exp (double (log x))))

(define (double x) (+ x x))

所以,一个程序的定义应该能够隐藏它的实现细节。程序的用户可以不用写程序的本身,
却可以从其他的程序员那里得到它,把它作为一个黑盒。一个用户为了使用程序,应该不需要知道
这个程序是如何被实现的。

* 局部名称
一个程序的实现的一个细节是程序的用户不用关注
程序的实现者对于程序的形式参数的名称的选择。因此,如下的程序是没有区别的。

(define (square x) (* x x ))

(define (square y) (* y y ))

这个原则是一个程序的含义应该独立于仅被作者使用的参数名称。
从表面上看,这是很明显的,但是它的影响是深远的。这最简单的影响是一个程序的
形式参数的名称必须是程序体的局部有效的。例如,在我们的平方根程序中,
我们能使用square在good-enough?程序的定义内部。

(define (good-enough? guess x)
   (< (abs (- (square guess) x)) 0.0001))

good-enough?的作者的意图是确定第一个数的平方与第二个数的误差是否在给定的范围内。
我们看到good-enough?的作者使用 guess 引用第一个参数,x 引用第二个参数。
square的实际参数是guess.如果square的作者使用x引用那个参数,我们能看到good-enough?程序
中的x,必须不同于square程序中的那个x。运行程序 square必须不能影响到被good-enough?程序使用的那个x.
因为square程序计算完成之后,x的值可能被good-enough?程序需要。

如果这些形式参数没有局限于它们的相应的程序的程序体内,那么,square程序的参数x
能与good-enough?程序中的x,混淆并且good-enough?程序的行为将依赖于我们使用的square程序的版本。
因此,square程序将不在是我们期望的黑盒了。

一个程序的形式参数在程序定义中,有一个特殊的角色,即不用关注形式参数有什么名称。
这样的名称被叫做绑定变量,我们说这个程序定义绑定了它的形式参数。 在定义之中,我们如果对于一个绑定的变量进行了一致性的重命名,那么程序下定义的含义是不变的。如果一个变量没有被绑定,我们说它是自由的。
定义的一个名称的绑定相关的表达式的集合 被叫做 这个名称的作用域。
在一个程序定义之中,声明为程序的形式参数的绑定变量的作用域是这个程序的程序体。

在如上的good-enough?程序的定义中,guess,x是绑定变量,但是< - abs,square是自由的。
good-enough?程序的含义独立于我们选择的guess,x的名称,只要它们是唯一的,且不同于< - abs,square。
(如果我们把guess重命名为abs,我们将引入了一个错误,因为它混淆了变量abs. 这个变量从自由状态
变成了绑定变量。) good-enough?程序的含义不独立于自由变量的名称。 good-enough?程序确实是依赖于一个事实。这个事实是符号abs命名了计算一个数的绝对值的程序。 如果我们在good-enough?程序的内部定义中
用cos替换了abs,good-enough?程序将计算一个不同的函数了。


* 内部定义和块结构

对于我们来说,有一种名称的隔离是有效的,例如程序的形式参数是局限于程序的程序体。
平方根的求解程序演示了我们要控制的名称的使用的另一种方式。已存在的程序
由独立的多个程序组成。

(define (sqrt x)
   (sqrt-iter 1.0 x)
)
(define (sqrt-iter guess x)
    (if (good-enough? guess x)
         guess
  (sqrt-iter (improve guess x) x)
     )
)
(defiine (good-enough? guess x)
    (< (abs (- (square guess) x))  0.0001)
)
(define (improve guess x)
        (average guess (/ x guess))
)

这个程序的问题是对于sqrt程序的用户来说仅有的重要的程序是sqrt程序。
其它的程序(sqrt-iter,good-enough?,improve)仅是对他们的干扰罢了。
他们不可能定义其它的程序来调用good-enough?来作为其它的程序的一部分,
这其它的程序和求平方根的程序一起工作,因为sqrt需要它。
在许多的独立的程序员在一起工作时,进行大型系统的构建时,问题更加严重。
例如,数据化的程序的大型的库的组装过程中,许多的数据函数被计算为连续的多个的估值,
因此可能有程序被命名为good-enough?和improve作为辅助程序。我们要局部化子程序,
把它们隐藏在sqrt程序的内部,让sqrt能和其它的连续估值的算法程序共存,任何一个程序都有自己的
私有的good-enough?程序。为了使这成为可能,我们能允许一个程序有内部的定义来局部化那个程序。
例如,在求平方根的程序中,我们能写成如下的样子:

(define (sqrt x)
  (define (sqrt-iter guess x)
    (if (good-enough? guess x)
         guess
  (sqrt-iter (improve guess x) x)
     ))
  (define (good-enough? guess x)
    (< (abs (- (square guess) x))  0.0001))
  (define (improve guess x)
        (average guess (/ x guess)))
   (sqrt-iter 1.0 x)
)

如此的级联的定义,叫做块结构,对于最简单化的命名包的问题,
是一个基本的正确的解决方案。但是这有一个更好的主意。除了内部化辅助程序的定义,
我们能简化它们。因为x被绑在sqrt的定义中,程序good-enough?,improve sqrt-iter定义在
sqrt的内部,是在x的作用域内。因此,没有必要显式地把它传给这些程序的任何一个。
替代的是,我们允许x成为内部定义的一个自由的变量,如下所示。然后,
x从外部的程序sqrt被调用时的参数得到值。这个原则叫做词法的作用域。


(define (sqrt x)
  (define (sqrt-iter guess)
    (if (good-enough? guess)
         guess
  (sqrt-iter (improve guess))
     ))
  (define (good-enough? guess)
    (< (abs (- (square guess) x))  0.0001))
  (define (improve guess)
        (average guess (/ x guess)))
   (sqrt-iter 1.0)
)

我们将使用块结构扩展性地帮助我们分解了大型程序成为易处理的小模块。
块结构的思想源于编程语言 Algol60.它出现在大多数的高级编程语言中,
是一个能够帮助组织大型程序的构建方面的重要的工具。

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81384716
今日推荐