【问题标题】:Why does Scheme allow mutation to closed environment in a closure?为什么Scheme允许在闭包中突变到封闭环境?
【发布时间】:2012-11-14 00:57:50
【问题描述】:

以下方案代码

(let ((x 1))
   (define (f y) (+ x y))
   (set! x 2)
   (f 3) )

计算结果为 5 而不是 4。令人惊讶的是,Scheme 促进了静态作用域。允许后续突变影响闭包中封闭环境中的绑定似乎恢复到有点动态范围。允许的任何具体原因?

编辑:

我意识到上面的代码不太明显地揭示了我所关心的问题。我在下面放了另一个代码片段:

(define x 1)

(define (f y) (+ x y))

(set! x 2)

(f 3)  ; evaluates to 5 instead of 4

【问题讨论】:

  • 我进行了更改以减少与本地范围的纠缠。

标签: scheme


【解决方案1】:

您在这里混淆了两个想法:作用域和通过内存进行间接寻址。词法作用域保证你对x的引用总是指向xlet绑定中的绑定。

这在您的示例中没有违反。从概念上讲,let 绑定实际上是在内存中创建一个新位置(包含1),该位置 是绑定到x 的值。当该位置被取消引用时,程序会在该内存位置查找当前值。当您使用set! 时,它会设置内存中的值。只有有权访问绑定到x 的位置(通过词法范围)的各方才能访问或改变内存中的内容。

相比之下,动态范围允许任何代码更改您在f 中引用的值,无论您是否授予对绑定到x 的位置的访问权限。例如,

(define f
  (let ([x 1])
    (define (f y) (+ x y))
    (set! x 2)
    f))

(let ([x 3]) (f 3))

将在具有动态范围的虚构方案中返回 6

【讨论】:

  • 你是对的。这不是动态范围(我说“有点”,也许我应该说“看起来”;)我在这里关心的是允许这种行为是否好。至少我看不到任何积极的后果。想象你定义了一个函数f,其中使用了一个全局变量x。后来你set!x 并打电话给f,它就会爆炸。在我看来,创建一个仅关闭环境而不是商店的关闭是一个糟糕的设计选择。
  • 好的,我想我现在更好地理解了你的问题。关闭商店还有其他问题:您如何确定要关闭多少商店?对于环境,从程序的文本中可以明显看出:只有范围内的引用。对于存储,如果f 调用gf 是否必须关闭g 访问的内存位置?此外,这将如何影响同时使用您关闭的相同内存位置的程序?
  • 我认为f 不必关闭g 可访问的商店,即使f 调用g,因为g 已经关闭了它。我还没想过涉及并发的可能后果。
【解决方案2】:

允许这种突变非常好。它允许您定义具有内部状态的对象,只能通过预先安排的方式访问:

(define (adder n)
  (let ((x n))    
    (lambda (y)
      (cond ((pair? y) (set! x (car y)))
            (else (+ x y))))))

(define f (adder 1))
(f 5)                 ; 6
(f (list 10))
(f 5)                 ; 15

没有办法改变x,除非通过f 函数及其已建立的协议——正是因为Scheme 中的词法 作用域。

x 变量指的是内部环境框架中属于 let 的内存单元其中定义了内部lambda - 因此返回lambda 和它的定义环境,也称为“闭包”。

如果你不提供改变这个内部变量的协议,没有什么可以改变它,因为它是 internal 并且我们早就离开了定义范围:

(set! x 5) ; WRONG: "x", what "x"? it's inaccessible!

编辑:您的新代码完全改变了您问题的含义,那里也没有问题。就像我们仍然在那个定义环境中一样,所以内部变量自然仍然可以访问。

比较有问题的是下面

(define x 1)
(define (f y) (+ x y))
(define x 4)
(f 5) ;?? it's 9.

我希望第二个定义不会干扰第一个,但R5RSdefine 就像顶级的set!

闭包将其定义环境与它们打包在一起。 始终可以访问顶级环境。

f 引用的变量x 位于顶级环境中,因此可以从同一范围内的任何代码访问。也就是说,任意代码。

【讨论】:

  • 我同意这个模拟对象功能是合理的。但请注意,您的示例中的set! 出现在f 中。自然它可以变异x(set! x 5) 这里会失败,但是因为xadder 的局部变量,也就是说,它不在作用域内。我已经更新了我的问题中的代码片段。您可以看到set! 出现在f 之外,但仍然可以改变在f 的闭包中关闭的变量。我反对的是这个用例。我认为仍然可以支持您举例说明的功能并禁止 set! 从闭包外部关闭闭包中的变量。
  • 我认为我的论点反对这种改变一个在闭包外部闭包中关闭的变量(你注意我之前的评论)仍然存在,无论顶层如何。
  • 感谢您的快速响应,:) 关于您的新编辑,如何将前两个定义放在一个文件中,加载它,然后设置或重新定义x,然后评估(f 5) .你会说我们还在定义环境中吗?
  • 文件的顶层也可能是这样的;关键是,closure 将 lambda 与它的定义环境打包在一起,并且顶层总是可以访问的,所以这样的闭包总是“开放的”。是的,在文件中,第二个定义就像 set! 一样,就像在 REPL 中一样。我正在使用 MIT Scheme - 不知道它是否依赖于实现。我希望第二个 x 不会影响第一个。
  • 但是您已经看到,在第一段代码中,即使封闭环境也不是始终可访问的顶级环境,您仍然可以从闭包外部对闭包中的变量进行变异。这就是为什么我认为封闭环境是否是顶级的在这里无关紧要。
【解决方案3】:

不,它不是动态范围。请注意,这里的define 是一个内部定义,只能由let 中的代码访问。具体来说,f 没有在模块级别定义。所以什么都没有泄露出去。

内部定义在内部实现为letrec (R5RS) 或letrec* (R6RS)。因此,它被视为相同(如果使用 R6RS 语义):

(let ((x 1))
  (letrec* ((f (lambda (y) (+ x y))))
    (set! x 2)
    (f 3)))

【讨论】:

    【解决方案4】:

    我的答案很明显,但我认为其他人没有触及它,所以让我说吧:是的,这很可怕。您在这里真正观察到的是,突变使您很难推断您的程序将要做什么。纯功能代码——没有突变的代码——在使用相同的输入调用时总是产生相同的结果。具有状态和突变的代码不具有此属性。使用相同的输入调用一个函数两次可能会产生不同的结果。

    【讨论】:

    • 感谢您的提醒,约翰,:)。但在这里,我对突变可能导致的问题以及如何限制它可能造成的麻烦特别感兴趣。
    • 不,OP 声称闭包内部的变量可以从外部变异是错误的。不能,有问题的变量在顶级范围内,而 everythinginside 顶级范围。问题不在于有状态编程与纯函数式编程的优点。这是具体的。
    • @plmday Scheme 限制“突变麻烦”的方法是引入新范围并关闭该 new 范围内的变量,以防止任何 “外部" 访问它 - 同时允许从 相同 范围(或任何“子”范围)内进行授权访问。
    • @plmday 让我们谈谈实现:你想复制闭包引用的所有存储值的表示吗?您也许可以做类似的事情,但我认为对于大多数人来说,使用不可变数据或确保没有对可变数据的外部引用更简单。
    • @plmday 只是不要那样定义它们,仅此而已。在子环境中定义它们 - 由 let 扩展(如我的答案所示,使用 adder(define f (let ((x ...)) (lambda ... x ...))) ) - 一切都会好起来的。仅当您真的需要这种行为(几乎从不)时,才使用对顶级变量的引用来定义它们。就这样。 IOW,这不是问题。 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-21
    • 1970-01-01
    • 1970-01-01
    • 2011-12-12
    • 1970-01-01
    相关资源
    最近更新 更多