使用 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) | ) +----------------------------------------------------------+