第五章,词法变量
=====
Scheme的变量有一定的词法作用域,即它们在程序代码中只对特定范围的代码结构可见。迄今为止我们所见过的全局变量也没有例外的:它们的作用域是整个程序,这也是一种特定的作用范围。
我们也碰见过一些示例包含局部变量。它们都是lambda过程的参数,当过程被调用时这些变量会被赋值,而它们的作用域仅限于在过程的内部。例如:
```scheme
(define x 9)
(define add2 (lambda (x) (+ x 2)))
x => 9
(add2 3) => 5
(add2 x) => 11
x => 9
```
这里有一个全局变量`x`,还有一个局部变量`x`,就是在过程`add2`中那个字母`x`。全局变量`x`的值一直是9。第一次调用`add2`过程时,局部的`x`会被赋值为3,而第二次调用`add2`时,局部变量`x`的会被赋值为全局变量`x`的值,即`9`。
当过程的调用结束时,全部变量`x`仍然是9。
而`set!`代码结构可修改变量的赋值。
```scheme
(set! x 20)
```
上面代码将全局变量`x`的值9修改为20,因为对于`set!`全局变量是可见的。如果`set!`是在`add2`过程体内被调用,那修改的就是局部变量`x`:
```scheme
(define add2
(lambda (x)
(set! x (+ x 2))
x))
```
这里`set!`在局部变量`x`上加上2,并且会返回局部变量x的新值。(从结果来看,我们无法区分这个过程和先前的`add2`过程)。
我们可以像先前一样使用全局的`x`做参数值来调用`add2`:
```scheme
(add2 x) => 22
```
(记住全局变量x的值现在是20,而不是9!)
`add2`过程内的`set!`调用仅会影响局部变量x。尽管局部变量x被赋了全局变量x的值,但后者不会因为`set!`为局部变量`x`赋值而受影响。
```scheme
x => 20
```
注意我们做这些讨论是因为我们为局部变量和全局变量使用了同样的标识`x`。在某些代码中,这个叫`x`的标识符指的是语法闭包中的局部`x`变量,这会暂时隐藏闭包外或全局变量`x`的值。例如,
```scheme
(define counter 0)
(define bump-counter
(lambda ()
(set! counter (+ counter 1))
counter))
```
`bump-counter`是一个没有参数的过程(没有参数的过程也称作`thunk`). 它没有引入局部变量和参数,这样就不会隐藏任何值。在每次调用时,它会修改全局变量`counter`的值,让它增加1,然后返回它当前的值。下面是一些`bump-counter`的成功调用示例:
```scheme
(bump-counter) => 1
(bump-counter) => 2
(bump-counter) => 3
```
## 5.1 let 和 let*
并不是一定要显式的创建过程才可以创建局部变量。有个特殊的代码结构let可以创建一列局部变量以便在其结构体中使用:
```scheme
(let ((x 1)
(y 2)
(z 3))
(list x y z))
=> (1 2 3)
```
和`lambda`一样,在`let`结构体中,局部变量`x`(赋值为1)会暂时隐藏全局变量`x`(赋值为20)。
局部变量`x`、`y`、`z`分别被赋值为1、2、3,这个初始化的过程并不作为`let`过程结构体的一部分。因此,在初始化时对`x`的引用都指向了全局变量`x`,而不是局部变量`x`。
```scheme
(let ((x 1)
(y x))
(+ x y))
=> 21
```
上面代码中,因为局部变量`x`被赋值为1,而`y`被赋上了值为20的全局变量`x`。
有时候,用`let`依次的创建局变量非常的方便,如果在初始化区域中可以用先创建的变量来为后创建的变量赋值也会非常方便。`let*`结构就可以这样做:
```scheme
(let* ((x 1)
(y x))
(+ x y))
=> 2
```
在初始化y变量时的x,指的是前面刚创建好的变量x。
这个例子完全等价于下面这个`let`嵌套的程序,更深了说,实际上就是`let`嵌套的缩写。
```scheme
(let ((x 1))
(let ((y x))
(+ x y)))
=> 2
```
我们也可以把一个过程做为值赋给变量:
```scheme
(let ((cons (lambda (x y) (+ x y))))
(cons 1 2))
=> 3
```
在这个`let`构结体中,变量`cons`将它的参数进行相加。而在`let`结构的外面,`cons`还是用来创建点对。
## 5.2 fluid-let
一个词法变量如果没有被隐藏,在它的作用域内一直都为可见状态。有时候,我们有必要将一个词法变量临时的设置为一个固定的值。为此我们可使用`fluid-let`结构(`fluid-let`是一个非标准的特殊结构。可参见8.3,在Scheme中定义fluid-let)。
```scheme
(fluid-let ((counter 99))
(display (bump-counter)) (newline)
(display (bump-counter)) (newline)
(display (bump-counter)) (newline))
```
这和`let`看起来非常相像,但并不是暂时的隐藏了全局变量counter的值,而是在`fluid-let`执行体中临时的将全局变量`counter`的值设置为了99直到执行体结束。因此执行体中的三句`display`产生了结果
```
100
101
102
```
当`fluid-let`表达式计算结束后,全局变量`counter`会恢复成之前的的值。
```scheme
counter => 3
```
注意`fluid-let`和`let`的效果完全不同。`fluid-let`不会和`let`一样产生一个新的变量。它会修改已经存的变量的值绑定,当`fluid-let`结束时这个修改也会结束。
为了清楚的说明这一些,可以思考这个根据前一个示例用`let`替换`fluid-let`后的程序。这次的输出是
```
4
5
6
```
即,初始值为3的全局变量`counter`,被每一次`bump-counter`的调用更新。而新创建的初始值为99的词法变量`counter`并没有影响到`bump-counter`的执行,因为尽管`bump-counter`是在局部变量`counter`的作用域内被调用的,但`bump-counter`的结构体并不在这个作用域内。所以`bump-counter`中的`counter`仍然指的是全局变量`counter`,最后的值为6。
```scheme
counter => 6
```