Elixir 变量真的是不可变的吗?
Are Elixir variables really immutable?
在 Dave Thomas 的《Programming Elixir》一书中,他说 "Elixir enforces immutable data" 并继续说:
In Elixir, once a variable references a list such as [1,2,3], you know it will always reference those same values (until you rebind the variable).
这听起来像 "it won't ever change unless you change it" 所以我对可变性和重新绑定之间的区别感到困惑。突出差异的示例将非常有帮助。
不变性意味着数据结构不会改变。例如,函数 HashSet.new
returns 是一个空集,只要您持有对该集的引用,它就永远不会变为非空。你可以在 Elixir 中做的是丢弃对某物的变量引用并将其重新绑定到新引用。例如:
s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>
不能发生的是该引用下的值在您未明确重新绑定它的情况下发生变化:
s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>
将此与 Ruby 进行对比,您可以在此处执行类似以下操作:
s = Set.new
s.add(:element)
s # => #<Set: {:element}>
不要将 Elixir 中的 "variables" 视为命令式语言中的变量,"spaces for values"。而是将它们视为 "labels for values".
当您查看变量 ("labels") 在 Erlang 中的工作方式时,您可能会更好地理解它。每当您将 "label" 绑定到一个值时,它就会永远绑定到它(范围规则当然适用于此)。
在 Erlang 中你不能这样写:
v = 1, % value "1" is now "labelled" "v"
% wherever you write "1", you can write "v" and vice versa
% the "label" and its value are interchangeable
v = v+1, % you can not change the label (rebind it)
v = v*10, % you can not change the label (rebind it)
相反,您必须这样写:
v1 = 1, % value "1" is now labelled "v1"
v2 = v1+1, % value "2" is now labelled "v2"
v3 = v2*10, % value "20" is now labelled "v3"
如你所见,这很不方便,主要是为了代码重构。如果你想在第一行之后插入一个新行,你必须重新编号所有 v* 或写类似 "v1a = ..."
所以在Elixir中可以重新绑定变量(改变"label"的意思),主要是为了方便:
v = 1 # value "1" is now labelled "v"
v = v+1 # label "v" is changed: now "2" is labelled "v"
v = v*10 # value "20" is now labelled "v"
总结: 在命令式语言中,变量就像命名的手提箱:你有一个名为 "v" 的手提箱。一开始你把三明治放进去。比你在里面放一个苹果(三明治丢了,可能被垃圾收集器吃掉了)。在 Erlang 和 Elixir 中,变量不是 一个地方 来放东西。它只是 一个 name/label 一个值。在 Elixir 中,您可以更改标签的含义。在 Erlang 中你不能。 这就是为什么在 Erlang 或 Elixir 中 "allocate memory for a variable" 没有意义的原因,因为变量不占用 space。值可以。 现在您可能清楚地看到了差异。
如果你想深入挖掘:
1) 查看 "unbound" 和 "bound" 变量在 Prolog 中的工作方式。这是 "variables which do not vary".
这个可能有点奇怪的 Erlang 概念的来源
2) 请注意,Erlang 中的“=”实际上不是赋值运算符,它只是一个匹配运算符!将未绑定变量与值匹配时,您将变量绑定到该值。匹配绑定变量就像匹配它绑定的值一样。所以这将产生 匹配 错误:
v = 1,
v = 2, % in fact this is matching: 1 = 2
3) 在 Elixir 中不是这样。所以在 Elixir 中必须有一个特殊的语法来强制匹配:
v = 1
v = 2 # rebinding variable to 2
^v = 3 # matching: 2 = 3 -> error
Erlang 和显然建立在它之上的 Elixir 都支持不变性。
他们根本不允许更改某个内存位置中的值。从不,直到变量被垃圾收集或超出范围。
变量不是一成不变的东西。他们指向的数据是不可变的。这就是为什么更改变量被称为重新绑定的原因。
你把它指向别的东西,而不是改变它指向的东西。
x = 1
后跟 x = 2
不会更改存储在计算机内存中的数据,其中 1 为 2。它将 2 放在新位置并指向 x
x
一次只能由一个进程访问,因此这对并发性没有影响,并且并发性是甚至关心某些东西是否不可变的主要地方。
重新绑定根本不会改变对象的状态,值仍然在相同的内存位置,但它的标签(变量)现在指向另一个内存位置,因此保留了不变性。重新绑定在 Erlang 中不可用,但在 Elixir 中,由于它的实现,这不会阻止 Erlang VM 强加的任何约束。
Josè Valim in this gist .
很好地解释了这种选择背后的原因
假设您有一个列表
l = [1, 2, 3]
并且您有另一个进程获取列表,然后对它们反复执行 "stuff" 并在此过程中更改它们是不好的。您可以像
一样发送该列表
send(worker, {:dostuff, l})
现在,您的下一段代码可能希望使用更多值更新 l 以进行与其他进程正在执行的操作无关的进一步工作。
l = l ++ [4, 5, 6]
哦不,现在第一个进程将有未定义的行为,因为您更改了列表,对吗?错误的。
原始列表保持不变。您真正做的是根据旧列表创建一个新列表,然后将 l 重新绑定到该新列表。
单独的进程永远无法访问 l。最初指向的数据 l 没有改变,另一个进程(大概,除非它忽略了它)有它自己对原始列表的单独引用。
重要的是您不能跨进程共享数据,然后在另一个进程正在查看数据时更改它。在像 Java 这样的语言中,你有一些可变类型(所有原始类型加上引用本身),可以共享一个 structure/object ,其中包含一个 int 并从一个线程更改该 int,而另一个是阅读它。
事实上,可以在另一个线程读取 java 中部分更改大整数类型。或者至少,它曾经是,不确定他们是否通过 64 位转换来限制这方面的事情。无论如何,要点是,您可以通过更改两者同时查看的地方的数据,将地毯从其他 processes/threads 下拉出来。
这在 Erlang 和扩展 Elixir 中是不可能的。这就是不变性在这里的意思。
更具体一点,在 Erlang(运行 VM Elixir 的原始语言)中,一切都是单赋值不可变变量,Elixir 隐藏了 Erlang 程序员开发的一种模式来解决这个问题。
在 Erlang 中,如果 a=3 那么 a 在该变量存在期间将是它的值,直到它退出范围并被垃圾收集。
这有时很有用(赋值或模式匹配后没有任何变化,因此很容易推断函数在做什么)但如果您在整个过程中对变量或集合执行多项操作,也会有点麻烦执行功能。
代码通常如下所示:
A=input,
A1=do_something(A),
A2=do_something_else(A1),
A3=more_of_the_same(A2)
这有点笨拙,使重构变得比需要的更困难。 Elixir 在幕后执行此操作,但通过编译器执行的宏和代码转换对程序员隐藏了它。
变量在某种意义上确实是不可变的,每个新的重新绑定(赋值)仅对之后的访问可见。所有以前的访问,在调用时仍然引用旧值。
foo = 1
call_1 = fn -> IO.puts(foo) end
foo = 2
call_2 = fn -> IO.puts(foo) end
foo = 3
foo = foo + 1
call_3 = fn -> IO.puts(foo) end
call_1.() #prints 1
call_2.() #prints 2
call_3.() #prints 4
让它变得非常简单
elixir 中的变量不像容器,您可以在容器中不断添加和删除或修改项目。
相反,它们就像贴在容器上的标签,当您重新分配一个变量时,您只需从一个容器中挑选一个标签并将其放置在一个包含预期数据的新容器上。
在 Dave Thomas 的《Programming Elixir》一书中,他说 "Elixir enforces immutable data" 并继续说:
In Elixir, once a variable references a list such as [1,2,3], you know it will always reference those same values (until you rebind the variable).
这听起来像 "it won't ever change unless you change it" 所以我对可变性和重新绑定之间的区别感到困惑。突出差异的示例将非常有帮助。
不变性意味着数据结构不会改变。例如,函数 HashSet.new
returns 是一个空集,只要您持有对该集的引用,它就永远不会变为非空。你可以在 Elixir 中做的是丢弃对某物的变量引用并将其重新绑定到新引用。例如:
s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>
不能发生的是该引用下的值在您未明确重新绑定它的情况下发生变化:
s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>
将此与 Ruby 进行对比,您可以在此处执行类似以下操作:
s = Set.new
s.add(:element)
s # => #<Set: {:element}>
不要将 Elixir 中的 "variables" 视为命令式语言中的变量,"spaces for values"。而是将它们视为 "labels for values".
当您查看变量 ("labels") 在 Erlang 中的工作方式时,您可能会更好地理解它。每当您将 "label" 绑定到一个值时,它就会永远绑定到它(范围规则当然适用于此)。
在 Erlang 中你不能这样写:
v = 1, % value "1" is now "labelled" "v"
% wherever you write "1", you can write "v" and vice versa
% the "label" and its value are interchangeable
v = v+1, % you can not change the label (rebind it)
v = v*10, % you can not change the label (rebind it)
相反,您必须这样写:
v1 = 1, % value "1" is now labelled "v1"
v2 = v1+1, % value "2" is now labelled "v2"
v3 = v2*10, % value "20" is now labelled "v3"
如你所见,这很不方便,主要是为了代码重构。如果你想在第一行之后插入一个新行,你必须重新编号所有 v* 或写类似 "v1a = ..."
所以在Elixir中可以重新绑定变量(改变"label"的意思),主要是为了方便:
v = 1 # value "1" is now labelled "v"
v = v+1 # label "v" is changed: now "2" is labelled "v"
v = v*10 # value "20" is now labelled "v"
总结: 在命令式语言中,变量就像命名的手提箱:你有一个名为 "v" 的手提箱。一开始你把三明治放进去。比你在里面放一个苹果(三明治丢了,可能被垃圾收集器吃掉了)。在 Erlang 和 Elixir 中,变量不是 一个地方 来放东西。它只是 一个 name/label 一个值。在 Elixir 中,您可以更改标签的含义。在 Erlang 中你不能。 这就是为什么在 Erlang 或 Elixir 中 "allocate memory for a variable" 没有意义的原因,因为变量不占用 space。值可以。 现在您可能清楚地看到了差异。
如果你想深入挖掘:
1) 查看 "unbound" 和 "bound" 变量在 Prolog 中的工作方式。这是 "variables which do not vary".
这个可能有点奇怪的 Erlang 概念的来源2) 请注意,Erlang 中的“=”实际上不是赋值运算符,它只是一个匹配运算符!将未绑定变量与值匹配时,您将变量绑定到该值。匹配绑定变量就像匹配它绑定的值一样。所以这将产生 匹配 错误:
v = 1,
v = 2, % in fact this is matching: 1 = 2
3) 在 Elixir 中不是这样。所以在 Elixir 中必须有一个特殊的语法来强制匹配:
v = 1
v = 2 # rebinding variable to 2
^v = 3 # matching: 2 = 3 -> error
Erlang 和显然建立在它之上的 Elixir 都支持不变性。 他们根本不允许更改某个内存位置中的值。从不,直到变量被垃圾收集或超出范围。
变量不是一成不变的东西。他们指向的数据是不可变的。这就是为什么更改变量被称为重新绑定的原因。
你把它指向别的东西,而不是改变它指向的东西。
x = 1
后跟 x = 2
不会更改存储在计算机内存中的数据,其中 1 为 2。它将 2 放在新位置并指向 x
x
一次只能由一个进程访问,因此这对并发性没有影响,并且并发性是甚至关心某些东西是否不可变的主要地方。
重新绑定根本不会改变对象的状态,值仍然在相同的内存位置,但它的标签(变量)现在指向另一个内存位置,因此保留了不变性。重新绑定在 Erlang 中不可用,但在 Elixir 中,由于它的实现,这不会阻止 Erlang VM 强加的任何约束。 Josè Valim in this gist .
很好地解释了这种选择背后的原因假设您有一个列表
l = [1, 2, 3]
并且您有另一个进程获取列表,然后对它们反复执行 "stuff" 并在此过程中更改它们是不好的。您可以像
一样发送该列表send(worker, {:dostuff, l})
现在,您的下一段代码可能希望使用更多值更新 l 以进行与其他进程正在执行的操作无关的进一步工作。
l = l ++ [4, 5, 6]
哦不,现在第一个进程将有未定义的行为,因为您更改了列表,对吗?错误的。
原始列表保持不变。您真正做的是根据旧列表创建一个新列表,然后将 l 重新绑定到该新列表。
单独的进程永远无法访问 l。最初指向的数据 l 没有改变,另一个进程(大概,除非它忽略了它)有它自己对原始列表的单独引用。
重要的是您不能跨进程共享数据,然后在另一个进程正在查看数据时更改它。在像 Java 这样的语言中,你有一些可变类型(所有原始类型加上引用本身),可以共享一个 structure/object ,其中包含一个 int 并从一个线程更改该 int,而另一个是阅读它。
事实上,可以在另一个线程读取 java 中部分更改大整数类型。或者至少,它曾经是,不确定他们是否通过 64 位转换来限制这方面的事情。无论如何,要点是,您可以通过更改两者同时查看的地方的数据,将地毯从其他 processes/threads 下拉出来。
这在 Erlang 和扩展 Elixir 中是不可能的。这就是不变性在这里的意思。
更具体一点,在 Erlang(运行 VM Elixir 的原始语言)中,一切都是单赋值不可变变量,Elixir 隐藏了 Erlang 程序员开发的一种模式来解决这个问题。
在 Erlang 中,如果 a=3 那么 a 在该变量存在期间将是它的值,直到它退出范围并被垃圾收集。
这有时很有用(赋值或模式匹配后没有任何变化,因此很容易推断函数在做什么)但如果您在整个过程中对变量或集合执行多项操作,也会有点麻烦执行功能。
代码通常如下所示:
A=input,
A1=do_something(A),
A2=do_something_else(A1),
A3=more_of_the_same(A2)
这有点笨拙,使重构变得比需要的更困难。 Elixir 在幕后执行此操作,但通过编译器执行的宏和代码转换对程序员隐藏了它。
变量在某种意义上确实是不可变的,每个新的重新绑定(赋值)仅对之后的访问可见。所有以前的访问,在调用时仍然引用旧值。
foo = 1
call_1 = fn -> IO.puts(foo) end
foo = 2
call_2 = fn -> IO.puts(foo) end
foo = 3
foo = foo + 1
call_3 = fn -> IO.puts(foo) end
call_1.() #prints 1
call_2.() #prints 2
call_3.() #prints 4
让它变得非常简单
elixir 中的变量不像容器,您可以在容器中不断添加和删除或修改项目。
相反,它们就像贴在容器上的标签,当您重新分配一个变量时,您只需从一个容器中挑选一个标签并将其放置在一个包含预期数据的新容器上。