Elixir变量真的是不可变的吗?

时间:2015-04-30 11:53:58

标签: immutability elixir

在Dave Thomas的书“编程Elixir”中,他指出“Elixir强制执行不可变数据”并继续说:

  

在Elixir中,一旦变量引用了诸如[1,2,3]之类的列表,你就会知道它将始终引用那些相同的值(直到你重新绑定变量)。

这听起来像“除非你改变它,它不会改变”所以我对可变性和重新绑定之间的区别感到困惑。突出差异的一个例子非常有用。

5 个答案:

答案 0 :(得分:84)

不要想到"变量"在Elixir中作为命令式语言的变量,"值的空间"。而是将它们视为"值的标签"。

当你看到变量("标签")如何在Erlang中工作时,也许你会更好地理解它。每当你绑定一个"标签"对于一个值,它永远与它保持联系(范围规则当然适用于此)。

在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中你可以重新绑定变量(更改"标签"的含义),主要是为了方便起见:

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中,变量不是一个地方来放置内容。它只是一个名称/标签的值。在Elixir中,您可以更改标签的含义。在Erlang你不能。 这就是为什么没有意义为"为变量分配内存"在Erlang或Elixir中,因为变量不占用空间。价值观。现在也许您可以清楚地看到差异。

如果你想深入挖掘:

1)看看" unbound"并且"绑定"变量在Prolog中有效。这就是这个可能有些奇怪的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

答案 1 :(得分:52)

不可变性意味着数据结构不会发生变化。例如,函数HashSet.new返回一个空集,只要您坚持对该集的引用,它就永远不会变为非空。你在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}>

答案 2 :(得分:32)

Erlang,显然是建立在它之上的Elixir,拥抱不变性。 他们根本不允许更改某个内存位置的值。从不直到变量被垃圾收集或超出范围。

变量不是不可变的东西。他们指出的数据是不可改变的。这就是更改变量的原因称为重新绑定。

你把它指向别的东西,而不是改变它指向的东西。

x = 1后跟x = 2不会更改存储在1为2的计算机内存中的数据。它会将2放在一个新位置并将x指向它

x一次只能由一个进程访问,因此这对并发性没有影响,并且即使有些东西是不可变的,并发也是最关心的主要场所。

重新绑定根本不会改变对象的状态,该值仍然在同一个内存位置,但它的标签(变量)现在指向另一个内存位置,因此保留了不变性。重新绑定在Erlang中不可用,但是当它在Elixir中时,由于其实现,这不会制动Erlang VM施加的任何约束。 JosèValimin this gist很好地解释了这种选择背后的原因。

假设您有一个列表

l = [1, 2, 3]

你有另一个进程正在采取列表,然后反复对它们执行“东西”,在这个过程中更改它们会很糟糕。您可以发送该列表,如

send(worker, {:dostuff, l})

现在,您的下一部分代码可能希望使用更多值更新l,以进行与其他进程无关的进一步工作。

l = l ++ [4, 5, 6]

哦不,现在第一个进程会有未定义的行为,因为你更改了列表吗?错误。

原始列表保持不变。你真正做的是根据旧列表创建一个新列表并将l重新绑定到新列表。

单独的进程永远无法访问l。最初指向的数据不变,另一个进程(可能,除非它忽略它)有自己单独的引用原始列表。

重要的是,您不能跨进程共享数据,然后在另一个进程正在查看它时更改它。在Java这样的语言中你有一些可变类型(所有基本类型加上引用本身),就可以共享一个包含int的结构/对象,并在另一个线程读取它时从一个线程更改int。

实际上,当它被另一个线程读取时,可以在java中部分更改一个大整数类型。或者至少,它曾经是,不确定他们是否通过64位转换来限制事物的这一方面。无论如何,重点是,您可以通过在同时查看的位置更改数据来从其他进程/线程下拉出地毯。

这在Erlang和Elixir中是不可能的。这就是不变性在这里意味着什么。

更具体一点,在Erlang(运行VM Elixir的原始语言)中,所有内容都是单一赋值不可变变量,Elixir隐藏了Erlang程序员开发的模式来解决这个问题。

在Erlang中,如果a = 3那么这就是该变量存在的持续时间的值,直到它退出范围并被垃圾收集。

这有时很有用(在赋值或模式匹配后没有任何变化,因此很容易推断出函数正在做什么)但如果你在执行一个变量或集合的过程中对变量或集合做了多件事,那也有点麻烦功能。

代码通常如下所示:

A=input, 
A1=do_something(A), 
A2=do_something_else(A1), 
A3=more_of_the_same(A2)

这有点笨拙,使得重构比实际需要更困难。 Elixir在幕后这样做,但是通过编译器执行的宏和代码转换将其隐藏在程序员之外。

Great discussion here

immutability-in-elixir

答案 3 :(得分:2)

变量在某种意义上确实是不可变的,每个新的重新绑定(赋值)只有在访问之后才可见。之前的所有访问权限仍然是在他们通话时引用旧值。

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

答案 4 :(得分:0)

使其非常简单

长生不老药中的变量与容器不同,在容器中,您不断从容器中添加,删除或修改项目。

它们就像贴在容器上的标签一样,当您重新分配变量很简单时,您可以从一个容器中选择一个标签,然后将其放置在其中包含预期数据的新容器中。