2.4 变量与Let表达式
假设expr
是一个表达式,其中包含一个变量var
,进一步,假设当我们对表达式expr
求值的时候,var
的值是val
。比如,当我们计算 (+ x 3)
的时候,我们希望 x
的值为2, 或者,当我们计算 (+ 2 y)
的时候,希望 y
的值为 3. 下面的示例展示了如何利用 Scheme 的 let
语法形式做到这一点。
(let ((x 2))
(+ x 3)) => 5
(let ((y 3))
(+ 2 y)) => 5
(let ((x 2) (y 3))
(+ x y)) => 5
let
语法结构包含一个由“变量-表达式”对构成的列表,后面跟着一系列的表达式,我们称之为 let
的 body
。通用的 let
语法结构如下:
(let ((var expr) ...) body1 body2 ...)
我们称之为,变量被 let
绑定到了值上. We refer to variables bound by let as let-bound variables.
let
表达式经常用来简化包含两个相同子表达式的表达式。这样也保证了公共的子表达式只被计算一次。
(+ (* 4 4) (* 4 4)) => 32
(let ((a (* 4 4))) (+ a a)) => 32
经常用方括号代替括号来分隔 let
表达式之中的多个绑定
(let ([list1 '(a b c)] [list2 '(d e f)])
(cons (cons (car list1)
(car list2))
(cons (car (cdr list1))
(car (cdr list2))))) => ((a . d) b . e)
Scheme 将方括号视为与圆括号相同,唯一的要求是它们必须配对。以方括号开始的列表必须以方括号闭合。我们会将方括号使用在let
及其它几个语法结构中,以改善可读性。
Since expressions in the first position of a procedure application are evaluated no differently from other expressions, a let-bound variable may be used there as well.
(let ([f +])
(f 2 3)) => 5
(let ([f +] [x 2])
(f x 3)) => 5
(let ([f +] [x 2] [y 3])
(f x y)) => 5
由 let
绑定的变量仅仅在 body 部分可见
(let ([+ *])
(+ 2 3)) => 6
(+ 2 3) => 5
这是幸运的,我们可不想在全局范围内将加法过程 +
给想修改成乘法 *
let
表达式是可以嵌套的
(let ([a 4] [b 3])
(let ([a-squared (* a a)]
[b-squared (* b b)])
(+ a-squared b-squared))) => 25
当我们在嵌套的let
表达式里对相同的变更重复绑定,内在层的body里,只有内层的绑定是可见的。
(let ([x 1])
(let ([x (+ x 1)])
(+ x x))) => 4
在外层的 let 表达式的 body 里,x 的值是 1, 而它的body,就是第二个let表达式。而第二个let
表达式又将 x 绑定为(+ x 1)
,于是,在内层的 let
表达式的body里,x 的值现在是 2。内层let
的body只有一个表达式,就是 (+ x x)
,于是我们得到 4 这个结果。
我们说,内层的 x
遮蔽了外层的绑定,一个let
绑定,在它的整个 body 里是完全可见的,除非它被遮蔽起来。变量可见的范围称作它的作用域,在上面的代码里,第一个 x 的作用域是外层 let 的 body,它的 body 即内层的 let 表达式。在内层的 let 表达式里, 第一个 x 被第二个 x 给遮蔽了。这种作用域形式叫做“词法作用域”。每一个绑定的作用域可以从程序的文本直接了当地分析出来。
多说一句,与之对应的是“动态作用域”,早期的 Lisp 语言与现在的 Emacs Lisp 采用的就是动态作用域。
通过使用不同的变量名,可以避免遮蔽的发生。上面的表达式可以将内部的 x 重写为 new-x
(let ([x 1])
(let ([new-x (+ x 1)])
(+ new-x new-x))) => 4
虽然使用不同的变量可以避免混淆,然而,遮蔽则可以避免意外地使用“旧”的值。
练习 2.4.1 重写下面的表达式,使用 let 去除共同的子表达式,以改善代码结构。注意,不要使用任何代数简化。
a. (+ (- (* 3 a) b) (+ (* 3 a) b))
b. (cons (car (list a b c)) (cdr (list a b c)))
练习 2.4.2 判断下面的表达式的值,解释你是怎么推导出值的
(let ([x 9])
(* x
(let ([x (/ x 3)])
(+ x x))))
练习 2.4.3 重写下面的表达式,给每个不同的 let 绑定变量不同的名字,使得没有变量被遮蔽,确认修改后的表达式的值与原来的表达式相同。
a.
(let ([x 'a] [y 'b])
(list (let ([x 'c]) (cons x y))
(let ([y 'd]) (cons x y))))
b.
(let ([x '((a b) c)])
(cons (let ([x (cdr x)])
(car x))
(let ([x (car x)])
(cons (let ([x (cdr x)])
(car x))
(cons (let ([x (car x)])
x)
(cdr x))))))