计算机程序的构造和解释(JavaScript版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1.5 函数应用的代换模型

在求值函数应用时,解释器完全按1.1.4节描述的过程工作。也就是说,解释器首先求值应用表达式的各个元素,而后把得到的函数(也就是该应用式中的函数表达式的值)应用于那些实际参数(它们就是应用式中的那些实参表达式的值)。

我们可以假定,基本函数的应用已经在解释器或库里做好了。对于复合函数,这种应用的计算过程是:

把一个复合函数应用于一些实际参数,就是在把该函数的返回表达式中的每个形参用相应的实参取代后,求值得到的结果表达式[11]

为了展示这个计算过程,让我们求值下面的应用式:

其中的f就是1.1.4节声明的那个函数。我们首先提取出f的返回表达式:

而后用实参5代换其中的形式参数:

这样,问题归约为对另一个应用式的求值,它有两个实参,函数表达式是sum_of_squares。求值这一应用式牵涉下面三个子问题:我们必须求值其中的函数表达式,得到应该去应用的函数;还要求值那些实参表达式,得到函数应用的实际参数。这里的5+1产生6,5 * 2产生10,因此我们需要把sum_of_squares函数应用于6和10。用这两个值代换sum_of_squares的返回表达式里的形式参数x和y,该表达式就归约为:

我们使用square的声明,又可以把它归约为:

通过乘法又能把它进一步归约为:

最后就得到了:

上面描述的计算过程称为函数应用的代换模型。在考虑本章到目前为止所关注的函数时,我们可以把它看作确定函数应用的“意义”的模型。但是,这里还需要强调两点:

●这里的代换只是为帮助我们思考函数调用的情况,而不是对解释器实际工作方式的具体说明。典型的解释器都不采用直接操作函数正文、以值代换形式参数的方式完成对函数调用的求值。实际的求值器通常采用为形式参数建立局部环境的方式产生“代换”的效果。我们将在第3章和第4章考察解释器的实现细节,更完整地讨论这个问题。

●随着本书的进展,我们将展示一系列有关解释器如何工作的模型,一个比一个更精细,并最终在第5章给出一个解释器和一个编译器的完整实现。代换模型只是这些模型中的第一个——作为形式化地思考求值过程的起点。一般而言,在模拟科学研究或者工程中的现象时,我们总是从简化的不完整的模型开始。随着更细致地检查所考虑的问题,这些简单模型也会变得越来越不合适,从而必须用进一步精化的模型取代。这里的代换模型也不例外。特别地,我们将要在第3章讨论把函数用于“变动数据”的问题,那时就会看到代换模型完全不行了,必须用更复杂的函数应用模型取代[12]

应用序和正则序

按1.1.4节给出的有关求值过程的描述,解释器首先求值函数和各个实参表达式,而后把得到的函数应用于得到的实际参数。然而,这并不是执行求值的唯一可能方式。另一种求值模型是先不求出实参表达式的值,直到实际需要它们的值的时候再求值。采用这种求值方式,我们就应该首先用实参表达式代换形式参数,直至得到一个只包含运算符和基本函数的表达式,然后再执行求值。如果我们采用这种方式,对下面这个表达式求值:

将会形成下面的逐步展开序列:

然后是下面的归约:

这样做也给出了与前面求值模型同样的结果,但计算过程却是不同的。特别地,对5+1和5 * 2的求值各做了两次,它们都出自对下面表达式的归约:

其中的x分别被代换为5+1和5 * 2。

这种“完全展开而后归约”的求值模型称为正则序求值,与之相对的是解释器实际使用的方式,“先求出实参而后应用”,这称为应用序求值。可以证明,对于可以用代换来模拟,并能产生合法值的那些函数应用(包括本书前两章的所有函数),正则序和应用序求值将产生同样的值(参看练习1.5中“非法”值的例子,正则序和应用序会给出不同的结果)。

JavaScript采用应用序求值,部分原因在于这样做能避免对表达式的重复求值(如上面的5+1和5 * 2的情况所示),从而可以提高一点效率。更重要的是,在超出了可以用代换方式模拟的函数的范围后,正则序的处理将变复杂许多。而在另一些方面,正则序也可以成为特别有价值的工具,我们将在第3章和第4章研究它的某些内在性质[13]