Elixir变量真的不可变吗?


72

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

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

这听起来像“除非您进行更改,否则它永远不会更改”,因此我对可变性和重新绑定之间的区别感到困惑。突出差异的示例将非常有帮助。


这是阴影,不是重新分配。
2015年

Answers:


60

不变性意味着数据结构不会改变。例如,该函数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}>

1
那么重新绑定是否像仅限本地的可变性一样?在一个块中,您可以重新绑定变量,但是一旦超出范围,该变量将返回其原始值-是吗?
Odhran Roche

1
仅限本地-是的。但是,当变量超出范围时,它只会停止存在。变量指向的数据不一定(例如可能从函数返回),并且该数据是不可变的。
帕维尔Obrok

1
如果您想要不可变的变量,则需要使用Erlang或在Elixir变量前面加上^。Rebind只是Elixir中一个花哨的名词,用来掩盖变量确实是可变的。请记住,我爱Elixir,但是当社区尝试将变量的可变性隐藏在花哨的术语和解释后面时,我真的不喜欢。
Exadra37 '19

1
@ Exadra37不是。您可以认为^表达式中的pin运算符只是用变量的值代替变量。该运算符发现它大部分用于模式匹配等。至于不可变性,是的,对于所有意图和目的,Elixir确实是不可变的。如果要更改变量的值,则该变量所指向的位置中存储的值本身不能突变为另一值。取而代之的是将变量反弹到可以存储新值的新位置。因此,术语“重新绑定”。
Sri Kadimisetty

1
@sri我不在乎实现细节,也就是核心如何工作,我不在乎接口,也就是如何使用它,因此,如果我可以两次使用同一个变量并获得不同的值,那么对我来说不是不变的,而是可变的。现在,您可以提供所需的所有技术说明,并且我知道很多,但是这永远不会改变,使用变量的Elixir API允许使用可变变量,但是使用变量的Erlang API仅允许使用不可变变量。
Exadra37

94

不要将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"

简介:在命令式语言中,变量就像命名为“ cases”的手提箱:您有一个名为“ v”的手提箱。首先,您将三明治放入其中。比您在其中放一个苹果(三明治丢失了,也许被垃圾收集器吃了)。在Erlang和Elixir中,变量不是放置某些内容的地方。它只是值的名称/标签。在Elixir中,您可以更改标签的含义。在Erlang中,您不能。这就是为什么在Erlang或Elixir中“为变量分配内存”没有意义的原因,因为变量不占用空间。价值观做到了。现在也许您清楚地看到了区别。

如果您想更深入地研究:

1)查看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
@DavidC:这是两个问题:1.是否可能/可以想象?2.这是个好主意吗?我认为1的答案是“是”,2的答案是“否”。有非常好的功能机制(map,reduce等),它们更适合imho。但是这个问题很广泛,可能无法用几句话就能回答:)
Miroslav Prymek

@MiroslavPrymek在答案的第3点,在Elixir中是否存在严格强制匹配的情况?
simo

@simo不知道。该手册也没有这样说:elixir-lang.org/getting-started/pattern-matching.html
Miroslav Prymek

4
x = x +1伤了我的数学头脑。Erlang止住了头痛,而Elixir没有。:(
凯文·蒙克

38

Erlang和显然基于它的Elixir都具有不变性。 它们只是不允许更改某个内存位置中的值。从不直到变量被垃圾回收或超出范围。

变量不是一成不变的东西。他们指向的数据是不变的。这就是为什么更改变量称为重新绑定。

您将其指向其他内容,而不更改其指向的内容。

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

x 一次只能由一个进程访问,因此这对并发性没有影响,并且并发是即使始终无法更改某些事物的主要场所。

重新绑定完全不会改变对象的状态,该值仍位于相同的内存位置,但是它的标签(变量)现在指向另一个内存位置,因此保留了不变性。重新绑定在Erlang中不可用,但是在Elixir中,重新绑定并不能克服Erlang VM实施的任何约束。何塞·瓦里姆(JosèValim)在这个要点中很好地解释了做出此选择的原因。

假设您有一个清单

l = [1, 2, 3]

并且您还有另一个过程在获取列表,然后反复对它们执行“填充”操作,并且在此过程中进行更改很不好。您可以像这样发送该列表

send(worker, {:dostuff, l})

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

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

哦,不,由于更改了列表,因此第一个过程将具有未定义的行为?错误。

该原始列表保持不变。您真正要做的是根据旧列表创建一个新列表,然后将其重新绑定到该新列表。

单独的进程永远无法访问l。最初指向的数据l保持不变,并且其他过程(大概,除非忽略了它)对原始列表有自己的单独引用。

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

实际上,在另一个线程读取Java时,可以部分更改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在后台进行此操作,但通过宏和编译器执行的代码转换将其隐藏在程序员面前。

这里很棒的讨论

el不变性


6
+1非常明确的答案。它很好地解释了不变性的基础是哪种编译器技术及其原因。最后,这个答案与Prymek的答案一起使我对这个问题有了很好的理解。它们都应该是Elixir官方文档的一部分。
Guido

@subhash,所以您是说x = 1; f = fn -> x end; x = 2; #=> 2在Elixir中进行操作时,flambda甚至不通过x变量访问x而是通过二级引用访问?即使x设置为2,它仍然能够访问1吗?我意识到部分原因是由于elixir立即对其进行了编译,但除此之外,即使在重新绑定发生之后,是否存在用于访问数据的辅助引用?
高个子男孩

1
这个答案除其他外,将尽最大的努力来解释这个话题
Srle

1
这对我来说是一个很好的解释!变量是Erlang中可变的东西,关键是要理解Erlang强制每个变量仅由它创建它的线程使用,因此在该可变的东西中不会发生并发。
Pauls

1
很棒的帖子,很明显,尤其是:Elixir在后台进行此操作,但是通过宏和编译器执行的代码转换将其隐藏在程序员面前。
Ace.Yin

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

0

使其非常简单

lix剂中的变量与容器不一样,您可以在容器中不断添加和删除或修改项目。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.