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

时间:2009-12-15 18:43:55

标签: functional-programming lisp clojure closures

有很多与此相关的SO帖子,但我再次以不同的目的问这个问题

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

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

在语言Clojure中,我可以执行以下操作:但由于值是不可变的,因此不会出现此问题。那么其他语言怎么样?闭包的正确技术定义是什么?

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

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

8 个答案:

答案 0 :(得分:9)

这不是那种似乎在这里起床的答案,但我衷心地敦促你通过阅读Shriram Krishnamurthi(免费!)(在线!)教科书{{3}来发现你的问题的答案。 }}

我将简要地解释一下非常非常这本书,总结了它引导你完成的极小解释者的发展:

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

这是关键点:你实现了这个,然后你发现使用替换它可以正常工作,但是在环境中它被破坏了。特别是,为了解决这个问题,您必须确保将评估的函数定义与评估时所使用的环境相关联。这对(fundef +定义环境)就是所谓的“闭包”。

呼!

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

我再次强烈建议您查看Programming Languages: Application and Interpretation

答案 1 :(得分:5)

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

(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”时,我们的意思是它引用了一个stricture / class /在编译时存储foo状态的任何东西。您需要关闭某些内容的原因是因为包含它的函数在使用数据之前就会消失。在这种情况下,外部(包含foo的值)将结束并在foo使用之前消失很长时间,因此没有人会修改foo。当然,foo可能会给那些可以修改它的人提供参考。

答案 2 :(得分:5)

词法闭包是一个附带的变量(例如你的例子中的greeting-prefix)被引用括起来的闭包。创建的闭包不是简单地在创建时获取greeting-prefix的值,而是获取引用。如果在创建闭包后修改了greeting-prefix,则每次调用闭包时都会使用它的新值。

在纯函数式语言中,这并不是一个区别,因为值永远不会改变。因此,greeting-prefix的值是否被复制到闭包中并不重要:引用原始版本与其副本之间的行为没有可能存在差异。

在“insrative-languages-with-closures”中,例如C#和Java(通过匿名类),必须做出一些关于封闭变量是否被值或引用括起来的决定。在Java中,这个决定只是允许封装final个变量而被抢占,就变量而言,有效地模仿了一个函数式语言。在C#中,我认为这是另一回事。

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

答案 3 :(得分:3)

答案 4 :(得分:2)

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

如果您愿意,可以将环境视为字典或哈希表。一个闭包得到它自己的小字典,在那里查找名字。

答案 5 :(得分:2)

您可能喜欢阅读On lambdas, capture, and mutability,其中介绍了如何在C#和F#中进行比较。

答案 6 :(得分:1)

查看此博文:ADTs in Clojure。它显示了一个很好的闭包应用程序,用于锁定数据,以便只能通过特定的接口访问它(使数据类型不透明)。

这种类型的锁定背后的主要思想更简单地用反例来说明,在我撰写这个答案时,huaiyuan在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").

答案 7 :(得分:0)

  

你可以把闭包想象成一个   “环境”,其名称是   受价值束缚。那些名字是   完全私密的封闭,这   这就是为什么我们说它“关闭”   它的环境。所以你的问题   没有意义,因为   “外面”不能影响   封闭的环境。是的,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)

并且 - 因为它们显然是可能的 - 我认为这个问题是合法的。