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

发布于 2024-08-15 18:12:43 字数 425 浏览 4 评论 0原文

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

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

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

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

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

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

There are many SO posts related to this, but I am asking this again with a different purpose

I am trying to understand why closures are important and useful. One of things that I've read in other SO posts related to this is that when you pass a variable to closure, the closure starts remembering this value from then onwards. Is this the entire Technical aspect of it or there is more to what happens there.

What I wonder then is what would happen when the variable used inside the closure gets modified from outside. Should they be constants only?

In the language Clojure, I can do the following: But since there are value is immutable, this issue does not arise. What about other languages and what is the proper technical definition of a closure?

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

终止放荡 2024-08-22 18:12:44

这不是那种似乎会在这里获得支持的答案,但我衷心敦促您通过阅读 Shriram Krishnamurthi 的(免费!)(在线!)教科书来找到您问题的答案,编程语言:应用和解释

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

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

这是关键点:您实现了这个,然后您发现通过替换它可以正常工作,但在环境中它会被破坏。特别是,为了修复它,您必须确保将求值函数定义与求值时所处的环境关联起来。这对(fundef + 定义环境)就是所谓的“闭包”。

哇!

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

我再次强烈建议您看看 PLAI

This is not the sort of answer that appears to get up-votes around here, but I would heartily urge you to discover the answer to your question by reading Shriram Krishnamurthi's (free!) (online!) textbook, Programming Languages: Application and Interpretation.

I will paraphrase the book very, very briefly, by summarizing the development of the teeny tiny interpreters that it leads you through:

  • an arithmetic expression language (AE)
  • an arithmetic expression language with named expressions (WAE);
    implementing this involves developing a substitution function that can
    replace names with values
  • a language that adds first-order functions (F1WAE): using a function involves substituting
    values for each of the parameter names.
  • The same language, without substitution: it turns out that "environments" allow you to avoid the overhead of pre-emptive substitution.
  • a language that eliminates the separation between functions and expressions by allowing
    functions to be defined at arbitrary locations (FWAE)

This is the key point: you implement this, and then you discover that with substitution it works fine, but with environments it's broken. In particular, in order to fix it up, you must be sure to associate with an evaluated function definition the environment that was in place when it was evaluated. This pair (fundef + environment-of-definition) is what's called a "closure".

Whew!

Okay, what happens when we add mutable bindings to the picture? If you try this yourself, you'll see that the natural implementation replaces an environment that associates names with values with an environment that associates names with bindings. This is orthogonal to the notion of closures; since closures capture environments, and since environments now map names to bindings, you get the behavior you describe, whereby mutation of a variable captured in an environment is visible and persistent.

Again, I would very much urge you to take a look at PLAI.

五里雾 2024-08-22 18:12:44

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

(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 状态的严格/类/任何内容。您需要关闭某些内容的原因是因为包含它的函数在使用数据之前就消失了。在这种情况下,outer(包含 foo 的值)将在 foo 使用之前结束并消失,因此没有人会修改 foo。当然 foo 可以将引用传递给可以修改它的人。

A closure is really a data structure used by the compiler to make sure that a function will always have access to the data that it needs to opperate. here is an example of a function that recordes when it was defined.

(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"

when this function is defined a class is created and a small structure (a closure) is allocated on the heap that stores the value of foo. the class has a pointer to that (or it contains it im not sure). if you run this again then a second closure would be allocated to hold that other foo. When we say "this function closes over foo" we mean to say that it has a reference to a stricture/class/whatever that stores the state of foo at the time it was compiled. The reason you need to close over something is because the function that contains it is going away before the data will be used. In this case outer (which contains the value of foo) is going to end and be gone long before foo is used so nobody will be around to modify foo. of course foo could pas a ref to somebody who could then modify it.

迷爱 2024-08-22 18:12:44

词法闭包是指其中所包含的变量(例如示例中的greeting-prefix)通过引用括起来的闭包。创建的闭包并不是简单地获取创建时的greeting-prefix的值,而是获取一个引用。如果在创建闭包后修改了greeting-prefix,则每次调用闭包时都会使用它的新值。

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

在“带有闭包的命令式语言”中,例如 C# 和 Java(通过匿名类),必须决定封闭的变量是通过值还是通过引用封闭。在 Java 中,这一决定是通过只允许包含 final 变量来抢占先机的,就该变量而言,有效地模仿了函数式语言。在 C# 中,我相信这是另一回事。

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

A lexical closure is one in which the enclosed variables (e.g. greeting-prefix in your example) are enclosed by reference. The closure created does not simply get the value of greeting-prefix at the time it is created, but gets a reference. If greeting-prefix is modified after the closure is created, then its new value will be used by the closure every time it is called.

In pure functional languages this isn't much of a distinction, because values are never changed. So it doesn't matter if the value of greeting-prefix is copied into the closure: there's no possible difference in behaviour that could arise from referring to the original versus its copy.

In "imperative-languages-with-closures", such as C# and Java (via anonymous classes), some decision has to be made about whether the enclosed variable is enclosed by value or by reference. In Java this decision is pre-empted by only allowing final variables to be enclosed, effectively mimicking a functional language as far as that variable is concerned. In C# I believe it is a different matter.

Enclosing by value simplifies the implementation: the variable to be enclosed will often exist on the stack and hence will be destroyed when the function constructing the closure returns -- that means it can't be enclosed by reference. If you need enclosure by reference, a workaround is to identify such variables and keep them in an object allocated each time that function is called. This object is then kept as part of the closure's environment and must remain live as long as all closures using it are live. (I do not know if any compiled languages directly use this technique.)

贱贱哒 2024-08-22 18:12:44

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

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

You can think of a closure as an "environment", in which names are bound to values. Those names are entirely private to the closure, which is why we say that it "closes over" its environment. So your question isn't meaningful, in that the "outside" cannot affect the closed-over environment. Yes, a closure can refer to a name in a global environment (in other words, if it uses a name that is not bound in its private, closed-over environment), but that's a different story.

If you like, you can think of an environment as a dictionary, or hash table. A closure gets its own little dictionary where names are looked up.

又怨 2024-08-22 18:12:44

您可能会喜欢阅读关于 lambda、捕获和可变性,其中描述了它在 C# 和 F# 中的工作原理,以进行比较。

You might enjoy reading On lambdas, capture, and mutability, which describes how this works in C# and F#, for comparison.

篱下浅笙歌 2024-08-22 18:12:44

看一下这篇博文:Clojure 中的 ADT。它展示了闭包在锁定数据问题上的一个很好的应用,以便可以通过特定的接口独占地访问数据(使数据类型不透明)。

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

Have a look at this blog post: ADTs in Clojure. It shows a nice application of closures to the problem of locking up data so that it is accessible exclusively through a particular interface (rendering the data type opaque).

The main idea behind this type of locking is more simply illustrated with the counter example, which huaiyuan posted in Common Lisp while I was composing this answer. Actually, the Clojure version is interesting in that it shows that the issue of a closed-over variable changing its value does arise in Clojure if the variable happens to hold an instance of one of the reference types.

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

As for the original make-greeter example, you could rewrite it thus (note the deref/@):

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

Then you can use it to render personalised greetings from the different operators of various sections of a website. :-)

((make-greeter "Hello from Gizmos Dept") "John")
((make-greeter "Hello from Gadgets Dept") "Jack").
蝶舞 2024-08-22 18:12:44

你可以将闭包视为
“环境”,其中名称是
与价值观绑定。这些名字是
闭包完全私有,其中
这就是为什么我们说它“结束了”
它的环境。所以你的问题
没有意义,因为
“外部”不能影响
封闭的环境。是的,一个
闭包可以引用 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)

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

You can think of a closure as an
"environment", in which names are
bound to values. Those names are
entirely private to the closure, which
is why we say that it "closes over"
its environment. So your question
isn't meaningful, in that the
"outside" cannot affect the
closed-over environment. Yes, a
closure can refer to a name in a
global environment (in other words, if
it uses a name that is not bound in
its private, closed-over environment),
but that's a different story.

I suppose that the question was if things like these are possible in languages which allow mutation of local variables:

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)

And -- since they are obviously possible -- I think the question is legitimate.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文