Erlang 和显然是建立在它之上的 Elixir 包含不变性。
它们只是不允许某个内存位置中的值发生变化。 永远不会,直到变量被垃圾收集或超出范围。
变量不是不可变的东西。他们指向的数据是不可变的。这就是为什么更改变量被称为重新绑定。
你把它指向别的东西,而不是改变它指向的东西。
x = 1 后跟 x = 2 不会将存储在计算机内存中的数据从 1 更改为 2。它会将 2 放在一个新位置并将x 指向它。
x 一次只能由一个进程访问,因此这对并发性没有影响,而且并发性是主要关心的地方,甚至关心某些东西是否不可变。
重新绑定根本不会改变对象的状态,值仍然在同一个内存位置,但它的标签(变量)现在指向另一个内存位置,所以保持不变性。重新绑定在 Erlang 中不可用,但在 Elixir 中,这并没有阻止 Erlang VM 施加的任何约束,这要归功于它的实现。
Josè Valim in this gist 很好地解释了这种选择背后的原因。
假设你有一个列表
l = [1, 2, 3]
并且您有另一个进程正在获取列表,然后对它们反复执行“东西”并在此过程中更改它们会很糟糕。您可以发送该列表,例如
send(worker, {:dostuff, l})
现在,您的下一段代码可能希望用更多值更新 l 以进行与其他进程正在执行的操作无关的进一步工作。
l = l ++ [4, 5, 6]
哦,不,现在第一个进程将具有未定义的行为,因为您更改了列表,对吗?错误的。
原始列表保持不变。你真正做的是在旧列表的基础上创建一个新列表,并将 l 重新绑定到那个新列表。
单独的进程永远无法访问 l。 l 最初指向的数据没有改变,并且其他进程(大概,除非它忽略它)有自己对原始列表的单独引用。
重要的是您不能跨进程共享数据,然后在另一个进程正在查看数据时更改它。在像 Java 这样的语言中,你有一些可变类型(所有原始类型加上引用本身),可以共享一个包含一个 int 的结构/对象,并在另一个线程读取它时从一个线程更改该 int。
事实上,在 java 中,可以在另一个线程读取大整数类型时部分更改它。或者至少,它曾经是,不确定他们是否通过 64 位转换限制了这方面的事情。无论如何,重点是,您可以通过在两者同时查看的地方更改数据来从其他进程/线程下拉出地毯。
这在 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 在幕后做这件事,但通过宏和编译器执行的代码转换对程序员隐藏它。
Great discussion here
immutability-in-elixir