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 语法结构包含一个由“变量-表达式”对构成的列表,后面跟着一系列的表达式,我们称之为 letbody。通用的 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。内层letbody只有一个表达式,就是 (+ 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))))))

results matching ""

    No results matching ""