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

Posted

技术标签:

【中文标题】Elixir 变量真的是不可变的吗?【英文标题】:Are Elixir variables really immutable? 【发布时间】:2015-07-10 02:49:06 【问题描述】:

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

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

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

【问题讨论】:

这是阴影,而不是重新分配。 遮蔽 is 在同一个函数体中重新赋值 ...afraict 【参考方案1】:

不要将 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) 看看“未绑定”和“绑定”变量在 Prolog 中是如何工作的。这就是“不变的变量”这个可能有点奇怪的 Erlang 概念的来源。

2) 请注意,Erlang 中的“=”实际上不是赋值运算符,它只是一个匹配运算符!将未绑定的变量与值匹配时,将变量绑定到该值。匹配绑定变量就像匹配它绑定的值一样。所以这会产生一个 match 错误:

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

【讨论】:

@DavidC:这是两个问题:1. 有可能/可以想象吗? 2. 这是个好主意吗?我认为 1 的答案是“是”,而 2 的答案是“否”。有非常好的功能机制(map,reduce,...)更适合恕我直言。但是这个问题很广泛,可能无法用几句话来回答:) @MiroslavPrymek 在您回答的第 3 点,Elixir 中是否存在严格强制匹配的情况? @simo 不,我知道。手册也没有这样说:elixir-lang.org/getting-started/pattern-matching.html x = x +1 伤害了我的数学大脑。 Erlang 停止了这种头痛,而 Elixir 却没有。 :(【参考方案2】:

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

【讨论】:

那么重新绑定就像本地可变性一样吗?在一个块中,您可以重新绑定一个变量,但一旦超出范围,该变量就会恢复到其原始值 - 对吗? 仅限本地 - 是的。但是当变量超出范围时,它就会停止存在。变量指向的数据不一定(例如可能从函数返回)并且该数据是不可变的。 如果你想要不可变的变量,你需要使用 Erlang 或在 Elixir 变量前加上 ^。 Rebind 只是 Elixir 中的一个花哨术语,用于隐藏变量确实是可变的。请记住,我喜欢 Elixir,但我真的不喜欢社区试图在花哨的术语和解释背后隐藏变量的可变性。 @sri 我不关心实现细节,也就是核心如何工作,我关心接口,也就是如何使用它,因此如果我可以使用相同的变量两次并得到一个不同的值,那么对我来说不是不可变的,而是可变的。现在你可以提供所有你想要的技术解释,我知道很多,但这永远不会改变使用变量的 Elixir API 允许可变变量,但使用变量的 Erlang API 只允许不可变变量. @Exadra37 elixir 变量是不可变的,因为重新绑定变量只是重用变量名:它引用不同的内存位置,并且 在重新绑定之前出现的任何代码仍将引用旧值,即使之后运行。可变性作为一个概念是指内存中的一个值,而不是引用哪个值。您可以将 elixir 的重新绑定视为一种编译时技巧,它(在重新绑定时和重新绑定之后)将重命名变量,以便 VM 不会尝试将值分配给已分配的变量(这实际上是 Elixir 编译器的行为方式) .【参考方案3】:

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

【讨论】:

+1 非常明确的答案。它很好地解释了不可变性背后的编译器技术及其原因。这个答案加上Prymek的答案终于让我对这件事有了一个很好的理解。它们都应该是 Elixir 官方文档的一部分。 @subhash 那么您是说当您在 Elixir 中执行 x = 1; f = fn -&gt; x end; x = 2; #=&gt; 2 时,f lambda 甚至不通过 x 访问 x 变量,而是通过辅助引用访问?即使 x 设置为 2,它仍然可以访问 1 吗?我意识到这部分来自于 elixir 立即编译它,但除此之外,即使在重新绑定发生之后,是否还有用于访问数据的辅助引用? 这个答案在这里最能解释这个话题 这对我来说是一个很好的解释!变量是 Erlang 中的可变事物,关键是要理解 Erlang 强制每个变量仅由它创建的线程使用,因此在可变事物中不会发生并发。 好帖子,很清楚,尤其是:Elixir 在幕后做这件事,但通过宏和编译器执行的代码转换向程序员隐藏了它。【参考方案4】:

变量在某种意义上确实是不可变的,每个新的重新绑定(分配)只对之后的访问可见。所有之前的访问,在调用时仍然引用旧值。

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

【讨论】:

【参考方案5】:

让它变得非常简单

elixir 中的变量不像容器,您可以在其中不断添加、删除或修改容器中的项目。

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

【讨论】:

以上是关于Elixir 变量真的是不可变的吗?的主要内容,如果未能解决你的问题,请参考以下文章

在Java中String类为什么要设计成final?String真的不可变吗?其他基本类型的包装类也是不可变的吗?

原始类型变量在 PL/SQL 代码中是不可变的吗?

ReadonlyCollection,对象是不可变的吗?

根据定义,值类型是不可变的吗?

Elixir 是不是具有类似于 Clojure 的持久数据结构?

Class的实例是不可变的吗?