定义变量和过程

学习用 define 定义变量与复合过程,理解定义时的求值时机、过程嵌套以及环境的概念。

导言

想象一种语言,其中我们不能用名字来指代计算对象。随着程序越来越复杂,跟踪计算每一步的细节会变得越来越困难、越来越不便。因此,我们通过 define 把值(即计算对象)赋给由名字标识的变量。这是 Racket 实现抽象最简单的手段。

虽然你在前面的小节中已经见过各种变量和过程定义,但我们还没有正式讲过如何使用 define。

首先,来看几个使用 define 的表达式示例。在 Racket 解释器中试试它们,看看会怎样。

-> (define x 5)
-> (define (square x) (* x x))
-> (define y (square 3))
-> (define z (+ x y))

定义变量

变量定义(variable definition)的一般形式如下:

(define [name] [value])

[name] 表示被赋值的变量。例如:

-> (define x 5)
-> x
5

x 是变量,5 是它的值。

[value] 可以是任何类型的值,甚至可以是表达式。变量定义有一个重要性质:定义的值会先被完全求值,再赋给变量。

-> (define x (+ 5 5))
-> x
10

为什么 x 不是 (+ 5 5)?因为定义 x 时,我们必须先把表达式 (+ 5 5) 求值为最简形式 10,然后把 10 赋给 x。

检验理解

当我们调用 (define x (/ 1 0)) 时会发生什么?

定义复合过程

过程定义(procedure definition)是比变量定义更强大的抽象技术:我们可以给一个复合操作命名,从而把它作为一个整体来引用。先从定义 square 过程这个简单例子开始:

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

我们可以这样理解它:

(define (square x) (   *     x      x))
   求   x 的平方,  即  用 x 乘 x。

过程定义的一般形式如下:

(define ([name] [formal parameters]) [body])

注意它与变量定义的一个显著区别:过程名和形参被括号绑定在一起。回想一下,除了引号之外,一对括号表示过程调用。我们可以这样理解这个形式:当我们用 [形参] 调用 [名字] 时,我们将执行 [过程体]。

检验理解

复合过程可以带有任意非负数量的形参,甚至是 0。如何正确定义一个名为 foo、不接收任何实参且返回 5 的过程?

过程定义有一个重要性质:过程体直到过程被调用时才求值。这意味着,定义 square 时,我们还不知道需要把 x 与它自身相乘。只有当我们对某个数(比如 3)调用 square 时,我们才知道需要计算 (* 3 3)。

检验理解

当我们调用 (define (x) (/ 1 0)) 时会发生什么?

警告

创建复合过程时,有一点需要特别注意,那就是命名。给过程和形参命名时务必小心。Racket 不接受重复定义,这意味着已经定义过的过程名不能再用作复合过程的名字或形参。下面是一个不允许的复合过程定义示例(在解释器中试试,看看为什么不行):

(define (foo sent word)
    (word sent word))

在过程体的两处引用中,word 指的是哪个——形参还是内置过程?

嵌套过程

回到嵌套表达式的概念,我们也可以把过程嵌套在其他过程定义里。正如你在作业 0-1 中做过的,我们可以在 sum-of-squares 的定义中使用 square 过程:

(define (sum-of-squares x y)
    (+ (square x) (square y)))

这大致可以翻译为:当我们对 x 和 y 调用 sum-of-squares 时,我们将把 x 的平方与 y 的平方相加。

小结

为澄清起见,

(define foo 10)
  • 这是变量定义。

  • foo 是变量名。

  • 10 是赋给 foo 的值。

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

  • 这是复合过程定义。

  • square 是过程名。

  • x 是它唯一的形参。

  • (* x x) 是它的过程体。

    -> (square 3) 9

  • 这是表达式,同时也是过程调用。

  • square 是过程,是本表达式的运算符。

  • 3 是 square 的实参,是本表达式的操作数。

  • 9 是返回值。

要点总结

  • 我们学到可以用 define 作为抽象的手段
  • 我们也学了如何定义变量和复合过程

把值和操作与符号关联起来、之后又能将它们检索出来,这种可能性意味着 Racket 解释器必须有某种存储器来跟踪这些关联对。我们称这个存储器为环境(environment),第 8 课会对此展开讲解。