关于闭包中的词法绑定的更多解释?

Posted

技术标签:

【中文标题】关于闭包中的词法绑定的更多解释?【英文标题】:More explanation on Lexical Binding in Closures? 【发布时间】:2010-12-26 22:33:57 【问题描述】:

有很多与此相关的 SO 帖子,但我出于不同的目的再次询问此问题

我试图理解为什么闭包很重要和有用。我在与此相关的其他 SO 帖子中读到的一件事是,当您将变量传递给闭包时,闭包从那时起开始记住该值。这是它的整个技术方面还是那里发生的更多。

我想知道的是,当闭包内部使用的变量从外部修改时会发生什么。它们应该只是常量吗?

在 Clojure 语言中,我可以做到以下几点: 但既然有值是不可变的,这个问题就不会出现。其他语言呢?闭包的正确技术定义是什么?

(defn make-greeter [greeting-prefix]
    (fn [username] (str greeting-prefix ", " username)))

((make-greeter "Hello") "World")

【问题讨论】:

【参考方案1】:

您可以将闭包视为一个“环境”,其中名称绑定到。这些名称对于闭包是完全私有的,这就是为什么我们说它“关闭”了它的环境。所以你的问题没有意义,因为“外部”不能影响封闭的环境。是的,闭包可以引用全局环境中的名称(换句话说,如果它使用的名称未绑定在其私有的封闭环境中),但这是另一回事。

如果您愿意,您可以将环境视为字典或哈希表。闭包有自己的小字典,用于查找名称。

【讨论】:

说闭包名称绑定到有点误导。在某些语言中(例如 Smalltalk),它们被绑定到 references。在 Smalltalk 中,两个闭包可以“关闭”一个局部变量 x,它们将看到彼此对 x 的分配。 同意,并且考虑到我的听众,我仔细考虑了我的用词选择。 :)【参考方案2】:

闭包实际上是编译器使用的一种数据结构,用于确保函数始终可以访问它需要操作的数据。这是一个记录定义时间的函数示例。

(defn outer []
    (let [foo (get-time-of-day)]
      (defn inner []
          #(str "then:" foo " now:" (get-time-of-day)))))


(def then-and-now (outer))
(then-and-now)    ==> "then:1:02:03 now:2:30:01"
....
(then-and-now)    ==> "then:1:02:03 now:2:31:02"

当这个函数被定义时,一个类被创建并且一个小结构(一个闭包)被分配在存储 foo 值的堆上。该类有一个指向它的指针(或者它包含它我不确定)。如果你再次运行它,那么将分配第二个闭包来保存另一个 foo。当我们说“这个函数在 foo 上关闭”时,我们的意思是说它引用了一个严格/类/在编译时存储 foo 状态的任何东西。您需要关闭某些内容的原因是因为包含它的函数在使用数据之前就消失了。在这种情况下,外部(包含 foo 的值)将在使用 foo 之前结束并消失很久,因此没有人会在附近修改 foo。当然 foo 可以将 ref 传递给可以修改它的人。

【讨论】:

【参考方案3】:

词法闭包是其中封闭变量(例如您的示例中的greeting-prefix)通过引用封闭的一种。创建的闭包在创建时并不会简单地获取greeting-prefix 的值,而是获取一个引用。如果greeting-prefix在闭包创建后被修改,那么闭包每次调用都会使用它的新值。

在纯函数式语言中,这并没有太大区别,因为值永远不会改变。因此,greeting-prefix 的值是否被复制到闭包中并不重要:引用原件和复制件可能产生的行为没有可能的差异。

在“带有闭包的命令式语言”中,例如 C# 和 Java(通过匿名类),必须做出一些决定来决定封闭的变量是由值封闭还是由引用封闭。在 Java 中,这个决定是先发制人的,只允许包含 final 变量,就该变量而言有效地模仿函数式语言。在 C# 中,我认为这是另一回事。

按值封闭简化了实现:要封闭的变量通常存在于堆栈中,因此在构造闭包的函数返回时将被销毁——这意味着它不能被引用封闭。如果您需要通过引用进行封装,解决方法是识别此类变量并将它们保存在每次调用该函数时分配的对象中。然后,该对象作为闭包环境的一部分保留,并且只要使用它的所有闭包都处于活动状态,就必须保持活动状态。 (我不知道是否有任何编译语言直接使用这种技术。)

【讨论】:

【参考方案4】:

您可能会喜欢阅读On lambdas, capture, and mutability,它描述了它在 C# 和 F# 中的工作原理,以便进行比较。

【讨论】:

【参考方案5】:

看看这篇博文:ADTs in Clojure。它展示了一个很好的闭包应用来解决锁定数据的问题,这样它就可以通过特定的接口独占访问(使数据类型不透明)。

这种类型的锁定背后的主要思想更简单地用反例来说明,怀远在我撰写此答案时在 Common Lisp 中发布了该示例。实际上,Clojure 版本的有趣之处在于它表明,如果变量恰好包含其中一种引用类型的实例,那么在 Clojure 中确实会出现封闭变量更改其值的问题。

(defn create-counter []
  (let [counter (atom 0)
        inc-counter! #(swap! counter inc)
        get-counter (fn [] @counter)]
    [inc-counter! get-counter]))

至于最初的 make-greeter 示例,您可以这样重写它(注意 deref/@):

(defn make-greeter [greeting-prefix]
    (fn [username] (str @greeting-prefix ", " username)))

然后您可以使用它来呈现来自网站各个部分的不同运营商的个性化问候。 :-)

((make-greeter "Hello from Gizmos Dept") "John")
((make-greeter "Hello from Gadgets Dept") "Jack").

【讨论】:

【参考方案6】:

更多描述参见示例:

Common Lisp HyperSpec, 3.1.4 Closures and Lexical Binding

Common Lisp the Language, 2nd Edition, Chapter 3., Scope and Extent

【讨论】:

【参考方案7】:

这不是似乎在这里获得投票的那种答案,但我衷心地敦促您通过阅读 Shriram Krishnamurthi 的(免费!)(在线!)教科书,Programming Languages: Application and Interpretation 找到问题的答案。 .

我将简要地解释这本书非常非常,通过总结它引导您完成的微小解释器的发展:

算术表达式语言 (AE) 具有命名表达式 (WAE) 的算术表达式语言; 实现这一点需要开发一个 substitution 函数,该函数可以 用值替换名称 一种添加一阶函数的语言 (F1WAE):使用函数涉及替换 每个参数名称的值。 相同的语言,无需替换:事实证明,“环境”可让您避免抢先替换的开销。 一种通过允许消除函数和表达式之间分离的语言 在任意位置定义的函数 (FWAE)

这是关键点:你实现了它,然后你发现替换它可以正常工作,但环境它被破坏了。特别是,为了修复它,您必须确保将评估的函数定义与评估时所处的环境相关联。这对(fundef + environment-of-definition)就是所谓的“闭包”。

哇!

好的,当我们向图片添加可变绑定时会发生什么?如果您自己尝试一下,您会发现自然实现将名称与值关联的环境替换为将名称与绑定关联的环境。这与闭包的概念是正交的;由于闭包捕获环境,并且由于环境现在将名称映射到绑定,因此您将获得您所描述的行为,从而在环境中捕获的变量的突变是可见且持久的。

再次,我强烈建议您查看PLAI。

【讨论】:

您的答案可以通过提供指向该书网站的超链接来改进——并不是说它太难找到,但这是一个不错的选择。【参考方案8】:

您可以将闭包视为 “环境”,其中名称是 绑定到价值观。这些名字是 完全私有的关闭,其中 这就是为什么我们说它“关闭” 它的环境。所以你的问题 没有意义,因为 “外”不能影响 封闭的环境。是的,一个 闭包可以引用 a 中的名称 全局环境(换句话说,如果 它使用未绑定的名称 其私密的封闭环境), 但那是另一回事。

我想问题是这样的事情在允许局部变量突变的语言中是否可行:

CL-USER> (let ((x (list 1 2 3)))
           (prog1
               (let ((y x))
                 (lambda () y))
             (rplaca x 2)))
#<COMPILED-LEXICAL-CLOSURE #x9FEC77E>
CL-USER> (funcall *)
(2 2 3)

而且——因为它们显然是可能的——我认为这个问题是合理的。

【讨论】:

以上是关于关于闭包中的词法绑定的更多解释?的主要内容,如果未能解决你的问题,请参考以下文章

关于无处不在的闭包

javascript 闭包 通俗解释

关于闭包函数和递归函数的详细理解

关于闭包

python中的闭包

JS你不知道的JavaScript 笔记—— 作用域与闭包 - 编译原理 - LHS - RHS - 循环与闭包 - 模块 - 词法作用域 - 动态作用域