方案环境模型关闭问题

scheme environment model closure issue

SICP 3.2引入环境模型来替代替换模型。

我在学习这部分时做了以下测试:

(define a1 1)
(define (f1) a1)
(f1) ; return 1
(define (f2) (define a1 2) a1)
(f2) ; return 2
(define (f3) (define a1 2) (f1))
(f3) ; return 1,not 2

最后的表情出乎我的意料

来自 SICP 的句子标记

A procedure object is applied to a set of arguments by constructing a frame, binding the formal parameters of the procedure to the arguments of the call, and then evaluating the body of the procedure in the context of the new environment constructed. The new frame has as its enclosing environment the environment part of the procedure object being applied.

根据这个规则,当在f3里面调用f1时,会创建一个新的环境,它的封闭环境是全局的,而不是调用时创建的环境(调用E1f2。对我来说,E1 应该是封闭环境。

为简洁起见,我画了两张描绘环境的图

一个类似的 C 例子:

int a = 1;
void f1() {
    printf("a = %d from f1\n", a);
}
void f2() {
    int a = 2;
    printf("a = %d from f2\n", a);
}
void f3() {
    int a = 2;
    f1();
}
int main() {
    f2();
    f3();
    return 0;
}
print:
a = 2 from f2
a = 1 from f1

有什么我遗漏的吗?或者为什么 f3SchemeC 中打印 1 而不是 2?

这里棘手的部分是 f2 中的内部定义。

如果您尝试这个程序,它使用 set! 而不是 define 你会得到你期望的结果(为了清楚起见,我重命名了最后一个 f3).

函数
(define a1 1)
(define (f1) a1)
(f1) ; return 1
(define (f2) (set! a1 2) a1)
(f2) ; return 2
(define (f3) (define a1 2) (f1))
(f3) ; return 1,not 2

输出:

1
2
2

现在您在原始程序中看不到该行为的原因是 内部定义:

 (define (f2) (define a1 2) a1)

内部定义将扩展为

 (define (f2) 
   (letrec ((a1 2))
     a1))

如果我们重命名变量是:

 (define (f2) 
   (letrec ((a2 2))
     a2))

因此内部定义将分配一个新变量,因此 a1 绑定到的原始位置不受影响 - 因此保持值 1。

注意:在 REPL(顶层)中使用 define 来定义一个已经定义的变量等同于 `set!。也就是说top-level define和internal define是两个不同的概念。

考虑在环境模型中,当你定义一个函数时,实际上你获得了一个闭包,这是一对(函数,环境,在其中评估free 函数的变量)。自由变量是函数中提到的变量,它不是参数或局部变量,因此,当函数实际执行时,会在闭包的环境部分中搜索它。

因此,当您定义 f1 时,返回的闭包具有当前全局环境作为环境,其中 a1 具有值 1

在您对 f2 的定义中,您将 a1 定义为 2。这是一个local的定义,所以在body中,首先在local环境中搜索a1的值,找到2[=63] =]

f3 的定义中,您再次将 a1 定义为 2,并且此绑定再次出现在本地环境中,但您调用 f1,这是一个闭包,在 its 执行期间,根据 f1 的定义搜索 a1 的值(找到的值是存在于环境中的值在闭包构建时使用,即 1.)

这种解释变量的方式称为静态绑定,与动态绑定相反,后者的结果是2.

请注意,C 和 Scheme 都使用静态绑定,您的 C 示例恰好显示了这一点:调用 f3 的结果是 1,因为这是打印在 [=10= 中的值].另一方面,图像不正确。您应该将环境视为一组框架,每个框架都包含连接到其他框架的绑定(即耦合变量、当前值)。这样f1f2f3的闭包就不同了[=​​63=]

下图为全球环境的成长:

E1a1定义后的全局环境。 E2f1 的定义之后,你可以注意到 f1 的值是一个指向第二帧的闭包(这是为了允许递归定义,因为 f1 可以在原理调用自身)。在闭包中 a1 的值是 E1 中存在的值。 E3f2定义后的环境,闭包先指向局部环境,其中a1等于2,再指向当前全局环境。最后E4就是f3定义后的全局环境。请注意,新闭包再次具有 a2 等于 2.

的局部环境

f3被调用时,其体内的f1被检索为E2f1的值,当f1被评估时, 然后使用 a1 = 1

另一方面,动态绑定不需要创建闭包。每个函数都在当前环境中进行评估,该环境是使用参数绑定和本地定义扩展的全局环境。所以你可以想象现在的全球环境只是这种形式:

但是当评估 f3 时,(define a1 2) 向环境添加一个新框架:

在这个环境中计算最后一个表格,(f1)。在此评估期间,在环境中检索 f1,并在 same(唯一)环境中再次评估 its 主体,在a2 的值为 2.