Local Vars and Environments

局部变量

使用 let, let*, letrec 都可以在当前环境中构造局部变量。这种 变量的生命会延续到这个环境消失为止。

这就像 C 语言里的

{  int x = 10;
   int y = 20;

   foo(x,y);
}

但是有一点不同就是,Scheme 的 let 生成的环境是分配在堆里而不 是像 C 那样分配在栈里的。所以 let 的局部变量有可能在 let 的 block 执行完毕以后还继续存在,只要有某些东西引用到它们。

这样我们可以制造一些返回函数的函数,这些函数拥有自己的状态记 忆,而这些记忆并不是全局变量,它们有点像 C 函数的 static 变 量。

下面是几个例子:

(define (function-gen n)
  (let ((local-var 0))
    (lambda ()
      (display "The local-var is ")
      (display local-var)
      (newline)
      (set! local-var (+ 1 local-var)))))

(define f1 (function-gen 0))
(define f2 (function-gen 100))

(f1)
(f2)

函数 function-gen 接受一个参数 n,并且把它保存到自己的局部变 量 local-var. 它返回一个新的函数,这个函数被调用就会打印 local-var 的值,并且把 local-var 的值加 1.

我们用 0 和 100 作为参数传递给 function-gen,生成了两个函数 f1 和 f2. 这是两个起点不同的计数器。f1 从 0 开始,而 f2 从 100 开始。每次被调用两个函数都打印自己的数字,并且加 1.

可见,f1 和 f2 所见到的 local-var 是两个不同的空间。也就是说, 每次调用 function-gen,都会由 let 生成一个新的变量 local-var, 这个变量将一直伴随新生成的函数。

局部函数

一个函数可以有自己配套的局部函数,这些函数可以从外层的 let, let* 或者 letrec 定义。局部函数的使用可以参考这里: TreeWalkerGenerator.

图示

我找到一个很好的图示,可以示意以下环境的生成。 我们用一个简单一些的例子:

(let ((x 10) (y 20))
          (+ x y))

执行前的环境

     +-----+        +------+-----+
envt |  * -+------->|  car |  *--+----> #<proc ...>#
     +-----+        +------+-----+
                    | cons |  *--+----> #<proc ...>#
                    +------+-----+
                    |   +  |  *--+----> #<proc ...>#
                    +------+-----+
                    |      *     |
                           * 
                    |      *     |
                    +------+-----+
                    |  foo |  +--+----> #<proc ...>#
                    +------+-----+

执行中

(let ((x 10) (y 20))
          (+ x y))

进入 let 之后,环境变化:

                    +-------+-----+
                    |  car  |  +--+----> #<proc ...>#
                    +-------+-----+
                    | cons  |  +--+----> #<proc ...>#
                    +-------+-----+
                    |   +   |  +--+----> #<proc ...>#
                    +-------+-----+
                    |       *     |
                            *
                    |       *     |
                    +-------+-----+
                    |  foo  |  +--+----> #<proc ...>#
                    +-------+-----+
                              /|\
                               |
                               |
                               |
     +-----+        +-------+--+--+
envt |  +--+------->|[scope]|  *  |
     +-----+        +-------+-----+             +-----+
                    |   x   |  10 |             | bar |
                    +-------+-----+             +-----+    
                    |   y   |  20 |    
                    +-------+-----+    

执行后

又回到第一幅图。

如果就这么结束了……

如果执行 let 后没有任何指针再指向这个环境,那么过一会儿 x, y 的绑定这个 frame 就会被垃圾回收掉。

如果中间生成了一个函数……

如果我们的代码不是那么简单,我们在 let 里生成了一个函数。比 如这样:

(define (func-gen)
  (let ((x 10) (y 20))
    (lambda (a b)
          (+ x y a b))))

(define bar (func-gen))

func-gen 函数中被调用时,它在 let 空间中生成了一个函数,并且 作为 func-gen 的返回值送到外层,它被绑定到最外层的环境中的 bar 变量。那么这个函数引用了这个环境,这个 let frame 不会被 回收。

bar 如果在外层层环境被调用,那么它的名字绑定环境仍然是 let 里面的环境。也就是说,它仍然可以使用局部变量 x 和 y!

如果我们调用

(f 1 2)

就得到结果 33.

同时赋值

注意 let 里的 binding 是这样产生的。首先,进入 let 时,我们 只看到外层的绑定,然后每个 let 绑定的右边被 eval,然后这些值 被放到临时的一些空间,所有的右边都求值完毕后,这些值被一一赋 给左边的名字。

这相当于同时赋值。

所以在下面这种情况里,内层的 let 绑定 b 时,实际上使用的是外 层的 x 在计算。

(let ((x 10)    ; bindings of x
      (a 20))   ; and a 
 +----------------------------------------------------------+
 | (foo x)                                 scope of outer x |
 | (let ((x (bar))                           and a          |
 |       (b (baz x x)))                                     |
 |  +------------------------------------------------+      |
 |  | (quux x a)                    scope of inner x |      |
 |  | (quux y b)                    and b            | )    |
 |  +------------------------------------------------+      |
 | (baz x a)                                                |
 | (baz x b)                                                | )
 +----------------------------------------------------------+