定义变量和过程
学习用 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 课会对此展开讲解。