过程应用的替换模型

讲解用替换模型理解复合过程的应用、形参命名的任意性,以及应用序与正规序两种求值次序的区别。

复合过程的应用

到目前为止,我们已经了解了 Racket 如何分解并求值诸如下面这样的表达式:

(+ 1 2)
(+ 3 4 (* 2 5))

求值遵循以下步骤:

  1. 求值过程
  2. 求值实参
  3. 把过程应用于实参

对于第 3 步,我们讲得有点含糊。到底怎么”应用”一个过程?对于 +、-、quote、or、and、not 这样的基本函数,我们可以认为它内置在解释器中。我们更关心的是更复杂的情况:如何应用复合(即用户自定义的)过程?因为我们可以定义任意多个复合过程,它们不可能全都内置在解释器里。所以需要有一种通用的、按部就班的方法来应用复合过程。思考这个问题的一种方式就是替换模型(Substitution Model),我们将在本小节中探讨它。

替换模型

用替换模型应用复合过程时,你要把过程体中的每个形参替换为相应实参的值,然后按常规方式求值。这到底是什么意思?通过一个例子来看会更清楚。

考虑第一次实验中出现的 sum-of-squares 过程,它可以这样定义:

(define (sum-of-squares x y)
  (+ (square x) (square y))) ;; 这一行是过程的"过程体"
(define (square x) (* x x))

替换模型如何处理 (sum-of-squares 3 4) 呢?

  1. 有一个形参 x,调用时传入实参 3;另一个形参 y,调用时传入实参 4。
  2. 我们把过程体中每一处 x 和 y 分别替换为 3 和 4。
  3. 过程体于是变成 (+ (square 3) (square 4))。
  4. 利用 square 的定义,它归约为 (+ (* 3 3) (* 4 4))。
  5. 应用两次乘法,得到 (+ 9 16)。
  6. 再应用加法,得到结果 25。

第 1 步和第 2 步是替换模型最核心的部分:找出传进函数的值,并把过程体中每一处变量替换为对应的值。

形参的名字

你现在可能已经注意到,形参的名字是任意的。例如,下面三种写法是等价的:

(define (square x) (* x x))
(define (square apple) (* apple apple))
(define (square magikarp) (* magikarp magikarp))

替换模型对这三种写法的处理完全一样,不过最好选一个容易理解的名字(这里第一种定义最理想)。关键是要在过程体内保持一致。例如下面这样就可能出错:

(define (square x) (* apple apple))
立方体形状的苹果

用上面的定义配合替换模型处理 (square 4) 时,你会发现有些东西没有妥善定义。过程 square 接受一个实参 x,这里是 4。那过程体怎么办呢?我们需要找到 apple 的值,才能计算 (* apple apple)。可是 apple 的值是什么?我们不知道!我们只知道 x 的值!

替换模型与 Racket

Racket 真的是用替换模型来应用复合过程的吗?并不完全是。我们用替换模型,是为了帮助思考过程应用。Racket 实际做的要稍微复杂一些,我们将在 Unit 3 和 Unit 4 中探讨。之后我们会发现,替换模型不足以解释 Racket 中的某些函数。不过,这个模型将作为一个框架,供我们在其上继续构建。

应用序与正规序

我们一直在用的求值方法——先求值运算符,再求值操作数,最后应用运算符——只是可能的求值规则之一。我们使用的这种次序称为应用序(applicative order)。另一种求值方法是:直到需要值时才求值操作数。这种方法称为正规序(normal order)。通过下面的例子可以看出两者的区别:

(square (+ 3 2))

注意,square 的输入是 (+ 3 2)。

应用序:

(square (+ 3 2))
(square 5)
(* 5 5)
25

在应用序中,你会先求值形参 x,再进入 square 的过程体,即 (* x x)。求值 (+ 3 2) 得到 5,这个 5 就是传进 square 的值。所以 x 被绑定(bound)到 5。

正规序:

(square (+ 3 2))
(* (+ 3 2) (+ 3 2))
(* 5 5)
25

在正规序中,直到万不得已,你才求值 (+ 3 2)。所以在这个例子中,(square x) 中的 x 被绑定到 (+ 3 2)。

请注意,在正规序中,由于 x(即 (+ 3 2))在需要之前不求值,它最终被求值了两次。另一方面,在应用序中,由于在应用过程之前先求值操作数 x,(+ 3 2) 只被求值一次。

考虑下面这段代码:

(define (double_first a b) (+ a a))

(double_first (+ 1 1) (+ 2 2))

在应用序中,(+ 1 1) 会被求值几次?

在正规序中,(+ 1 1) 会被求值几次?

在应用序中,(+ 2 2) 会被求值几次?

在正规序中,(+ 2 2) 会被求值几次?

答案与解析

在应用序中,(+ 1 1) 被求值 1 次,(+ 2 2) 也被求值 1 次(进入过程体前两个实参都先求值,但 b 的值在过程体中未被使用)。 在正规序中,(+ 1 1) 被求值 2 次(过程体 (+ a a) 用到两处 a),而 (+ 2 2) 一次都不会被求值(b 在过程体中从未被使用)。 这正是应用序与正规序的关键区别:正规序只在需要值时才求值,不需要的值可能完全不求值,而需要的值可能被重复求值。

要点总结

本小节的几个要点:

  • 替换模型有助于我们理解过程应用是如何工作的,但它并不是解释器实现过程应用的方式。
  • 先求值实参再应用过程(即应用序)只是一种求值方法。还有别的求值方法(如正规序),只在需要值的时候才求值实参。