绑定与分配

Binding vs Assignment

我已经阅读了许多关于 assignmentbinding 之间区别的文章,但还没有点击(特别是在命令式语言与没有突变的语言的上下文中) .

我在 IRC 上问过,有人提到这两个例子说明了区别,但后来我不得不去,但没有看到完整的解释。

有人可以详细解释一下how/why这是如何工作的,以帮助说明区别吗?

Ruby

x = 1; f = lambda { x }; x = 2; f.call
#=> 2

灵药

x = 1; f = fn -> x end; x = 2; f.()
#=> 1

绑定是指在基于表达式的语言中使用的特定概念,如果您习惯于基于语句的语言,它可能看起来很陌生。我将使用 ML 风格的示例来演示:

let x = 3 in
   let y = 5 in
       x + y

val it : int = 8

此处使用的 let... in 语法表明绑定 let x = 3 的范围仅限于 in 之后的表达式。同样,绑定 let y = 5 的范围仅限于表达式 x + y,因此,如果我们考虑另一个示例:

let x = 3 in
   let f () =
       x + 5
   let x = 4 in
       f()

val it : int = 8

结果仍然是 8,即使我们在对 f() 的调用之上有绑定 let x = 4。这是因为 f 本身被绑定在绑定 let x = 3 的范围内。

基于语句的语言中的赋值是不同的,因为被赋值的变量不在特定表达式的范围内,它们实际上是 'global' 对于它们所在的任何代码块,因此重新赋值变量会更改使用相同变量的评估结果。

Elixir 与 Ruby 可能不是最好的对比。在 Elixir 中,我们可以很容易地 "re-assign" 先前分配的命名变量的值。您提供的两个匿名函数示例演示了两种语言在其中分配局部变量的方式的差异。在 Ruby 中,变量(即内存引用)被赋值,这就是为什么当我们更改它时,匿名函数 returns 存储在该内存引用中的当前值。而在 Elixir 中,定义匿名函数时变量的值(而不是内存引用)被复制并存储为局部变量。

然而,在 Elixir 的 "parent" 语言 Erlang 中,变量通常是 "bound." 一旦你声明了名为 X 的变量的值,你就不能为程序的其余部分改变它,任何需要的改变都需要存储在新的命名变量中。 (在 Erlang 中有一种重新分配命名变量的方法,但不是习惯。)

以前听过这个解释,感觉还不错:

You can think of binding as a label on a suitcase, and assignment as a suitcase.

在其他语言中,如果您有赋值,则更像是将值放入手提箱。您实际上更改了手提箱中的值并放入了不同的值。

如果你有一个装有值的手提箱,在 Elixir 中,你可以在上面贴上标签。您可以更改标签,但行李箱中的价值仍然相同。

因此,例如:

iex(1)> x = 1
iex(2)> f = fn -> x end
iex(3)> x = 2
iex(4)> f.()
1
  1. 你有一个装有 1 的手提箱,你给它贴上了标签 x
  2. 然后你说,"Here, Mr. Function, I want you to tell me what is in this suitcase when I call you."
  3. 然后,你把装有1的手提箱的标签取下来,放在另一个装有2的手提箱上。
  4. 那你说"Hey, Mr. Function, what is in that suitcase?"

他会说“1”,因为行李箱没变。虽然,你已经把你的标签拿掉了,放在了另一个行李箱里。

理解差异的最简单方法是比较语言 interpreter/compiler 使用的 AST 生成 machine-/byte-code.

让我们从ruby开始。 Ruby 不提供开箱即用的 AST 查看器,因此我将使用 RubyParser gem:

> require 'ruby_parser'
> RubyParser.new.parse("x = 1; f = -> {x}; x = 2; f.()").inspect

#=> "s(:block, s(:lasgn, :x, s(:lit, 1)), 
#    s(:lasgn, :f, s(:iter, s(:call, nil, :lambda), 0, s(:lvar, :x))),
#    s(:lasgn, :x, s(:lit, 2)), s(:call, s(:lvar, :f), :call))"

我们要找的是第二行的最新节点:proc里面有x变量。换句话说,ruby 需要那里的绑定变量,名为 x。在计算 proc 时,x 的值为 2。因此 proc returns 2.

现在让我们检查一下 Elixir。

iex|1 ▶ quote do
...|1 ▶   x = 1
...|1 ▶   f = fn -> x end
...|1 ▶   x = 2
...|1 ▶   f.()
...|1 ▶ end

#⇒ {:__block__, [],
# [
#   {:=, [], [{:x, [], Elixir}, 1]},
#   {:=, [], [{:f, [], Elixir}, {:fn, [], [{:->, [], [[], {:x, [], Elixir}]}]}]},
#   {:=, [], [{:x, [], Elixir}, 2]},
#   {{:., [], [{:f, [], Elixir}]}, [], []}
# ]}

第二行的最后一个节点是我们的。它仍然包含 x,但在编译阶段,这个 x 将被评估为其当前分配的值。也就是说,fn -> not_x end 将导致编译错误,而在 ruby 中,proc 主体中可能存在任何内容,因为它会在 被调用时被计算

换句话说,Ruby 使用当前 调用者的 上下文来评估 proc,而 Elixir 使用闭包。它获取遇到函数定义的上下文并使用它来解析所有局部变量。

过了一会儿,我想出了一个答案,这可能是对“绑定”和“分配”之间区别的最好解释;它与我在另一个答案中写的内容没有任何共同之处,因此它作为一个单独的答案发布。

在任何函数式语言中,一切都是不可变的,术语“绑定”和“赋值”之间没有有意义的区别。无论哪种方式都可以称呼它;常见的模式是使用“绑定”一词,明确表示它是一个值绑定到一个变量。例如,在 Erlang 中,不能 rebound 一个变量。在 Elixir 中这是可能的(为什么,看在上帝的份上,José,为什么?)

考虑 Elixir 中的以下示例:

iex> x = 1
iex> 1 = x

以上是完全有效的 Elixir 代码。很明显,一个人不能给一个分配任何东西。它既不是转让也不是约束。 匹配。这就是 = 在 Elixir(和 Erlang)中的处理方式:如果两者都绑定 到不同的 值,则 a = b 失败;如果它们匹配,它 returns RHO;如果 LHO 尚未绑定,它将 LHO 绑定到 RHO。

在 Ruby 中有所不同。 赋值(复制内容)和绑定(引用)之间存在显着差异。