"pin" 运算符有什么作用?Elixir 变量是可变的吗?

47

目前正尝试理解 Elixir 中的 "^" 运算符。

当您对重新绑定变量没有兴趣,而是想匹配其匹配之前的值时,可以使用引用运算符 ^:

来源 - http://elixir-lang.org/getting_started/4.html

有了这个想法,您可以像这样附加一个新值到一个符号上:

iex> x = 1  # Outputs "1"
iex> x = 2  # Outputs "2"

我还可以做:

iex> x = x + 1  # Outputs "3"!

所以我的第一个问题是:Elixir变量可变吗? 看起来好像是这样的...但在一个函数式编程语言中应该可以实现吗?

现在我们来谈 "^" 运算符...

iex> x = 1  # Outputs "1"
iex> x = 2  # Outputs "2"
iex> x = 1  # Outputs "1"
iex> ^x = 2 # "MatchError"
iex> ^x = 1  # Outputs "1"

我认为 "^" 的作用是将 "x" 锁定到其最后绑定的值。就这么简单吗? 为什么不像 Erlang 本身一样确保所有的 'matches'/assignments 都是不可变的呢?

我刚刚才开始适应它...


3
重新分配后只是产生了阴影,数据仍然不可变。从 Joe Armstrong 的博客 中的闭包示例可以清楚地说明这一点。我认为这很令人困惑。 - Łukasz Ptaszyński
1
我认为在某个时候,我们需要向Elixir文档中添加一些内容来解释这个问题,因为这个问题经常被提出。 - Onorio Catenacci
2
我知道^运算符是必要的,才能在左侧项上执行模式匹配和赋值。但我真正不清楚的是为什么默认行为是“遮蔽/重新赋值”,而不是模式匹配:- 在我的代码中,我确实很少需要重新赋值;- 在一个声称具有不可变数据的代码中,我希望重新赋值是显式的。 - Pascal
1
@Pascal 因为数据是不可变的,所以没有副作用可以改变变量指向的内容。改变变量指向的唯一方法是重新分配它。由于这只会显式地发生,因此当您不再需要旧值时,不必想出新标识符是一种便利。 - greggreg
在第一条评论中提供了博客文章的工作链接:https://joearms.github.io/published/2013-05-31-a-week-with-elixir.html - Adam Millerchip
5个回答

63

Elixir中的数据仍然是不可变的,但有一些简写方式让你可以少打一些字或者不必担心找新名称。在Erlang中,您经常会看到以下代码:

SortedList = sort(List),
FilteredList = filter(SortedList),
List3 = do_something_with(FilteredList),
List4 = another_thing_with(List3)

在Elixir中,您只需编写:

list = sort(list)
list = filter(list)
list = do_something_with(list)
list = another_thing_with(list)

这两句话的意思完全相同,只是后者看起来更加美观。当然,最好的解决方法就是像这样写:

list |> sort |> filter |> do_something |> another_thing_with
每次将新的东西分配给list变量时,都会得到新的实例:
iex(1)> a = 1
1
iex(2)> b = [a, 2]
[1, 2]
iex(3)> a = 2
2
iex(4)> b
[1, 2] # first a did not change, it is immutable, currently a just points to something else

您只需简单地表示不再对旧的a感兴趣,然后让它指向其他内容。如果您来自Erlang背景,则可能知道shell中的f函数。

A = 1.
f(A).
A = 2.

在 Elixir 中,你不需要编写 f。这是自动完成的。这意味着每次在模式匹配的左侧有一个变量时,你都会为它分配一个新值。

如果没有 ^ 运算符,你将无法在模式匹配的左侧使用变量,因为它将从右侧获取新值。 ^ 表示:将其视为字面值,不要给该变量分配新值。

这就是为什么在 Elixir 中:

x = 1
[1, x, 3] = [1, 2, 3]

在Erlang中等同于:

X = 1,
[1, CompletelyNewVariableName, 3] = [1, 2, 3]

并且:

x = 1
[1, ^x, 3] = [1, 2, 3]

相当于:

x = 1
[1, 1, 3] = [1, 2, 3]

在Erlang中是这样实现的:

X = 1,
[1, X, 3] = [1, 2, 3]

我更愿意说 x = 1; [1, x, 3] = [1, 2, 3] 在 Erlang 中等同于以下代码:X = 1. f(X). [1, X, 3] = [1, 2, 3] - Patrick Oscity
3
在shell中可以使用,但是您也可以在脚本中使用它,这时f/1不可用,这就是我将其与创建新变量进行比较的原因。 - tkowal

26

Elixir中的数据是不可变的,但变量是可重新赋值的。让Elixir有点困惑的是你看到的组合赋值和模式匹配。

当你在左边使用等号与变量引用时,Elixir会首先进行模式匹配,然后执行赋值操作。当你只有一个单独的变量引用时,它将匹配任何结构并被赋值如下:

 a = 1 # 'a' now equals 1
 a = [1,2,3,4] # 'a' now equals [1,2,3,4]
 a = %{:what => "ever"} # 'a' now equals %{:what => "ever"}

如果在左侧有更复杂的结构,Elixir 会首先进行模式匹配,然后执行赋值操作。

[1, a, 3] = [1,2,3] 
# 'a' now equals 2 because the structures match
[1, a] = [1,2,3] 
# **(MatchError)** because the structures are incongruent. 
# 'a' still equals it's previous value

如果您想与变量的内容进行值匹配,您可以使用符号“^”:
a = [1,2] # 'a' now equals [1,2]
%{:key => ^a} = %{:key => [1,2]} # pattern match successful, a still equals [1,2]
%{:key => ^a} = %{:key => [3,4]} # **(MatchError)**

这个人为的例子也可以写成右边用'a'而没有引脚的形式:
%{:key => [1,2]} = %{:key => a}

现在假设你想要将一个变量赋值给结构体的一部分,但仅当该结构体的某一部分与存储在 'a' 中的内容匹配时,在Elixir中这是微不足道的:

a = %{:from => "greg"}
[message, ^a] = ["Hello", %{:from => "greg"}] # 'message' equals "Hello"
[message, ^a] = ["Hello", %{:from => "notgreg"}] # **(MatchError)**

在这些简单的例子中,引脚和模式匹配的使用并不是立刻非常有价值的,但随着你学习更多的Elixir并且开始越来越多地使用模式匹配,它将成为Elixir提供的表现力的一部分。

1
那个最后的代码块是我第一次看到使用pin操作符的例子,它并不像之前那些人为而难以理解的例子一样。 - Kenny Evitt

1

1

这是我简约的方法:

等号(=)不仅是赋值,还有两个操作:

  1. 模式匹配。
  2. 如果模式匹配成功,则从右到左进行赋值。否则报错。

把等号看作代数中的等号,表示等式两边相等,所以如果你有 x = 1,那么 x 的唯一值就是 1。

iex(1)> x = 1 # 'x' matches 1
1
iex(2)> x # inspecting the value of 'x' we get 1, like in other languages
1
iex(3)> x = 2 # 'x' matches 2
2
iex(4)> x # now 'x' is 2
2

那么我们如何使用 'x' 进行比较而不是为它赋新值呢?

我们需要使用位运算符 ^:

iex(5)> ^x = 3
** (MatchError) no match of right hand side value: 3

我们可以看到,'x'的值仍然是2。
iex(5)> x
2

1
最好理解Elixir的引用操作符“^”的方法是通过相关的例子。
问题:
用户可以在更改密码之前提供新密码和以前的密码。
解决方案:
在像JavaScript这样的语言中,我们可以编写一个简单的解决方案,如下所示。
let current_password = 'secret-1';

const params = {
  new_password: 'secret-2',
  current_password: 'secret-2'
}

if (current_password !== params.current_password) {
  throw "Match Error"
}

以上代码会抛出一个“匹配错误”,因为用户提供的密码与其当前密码不匹配。
使用 Elixir 的 pin 运算符,我们可以将上述代码编写为:
current_password = 'secret-1'

{ new_password, ^current_password } = { 'secret-2', 'secret-2'}

以上代码还会引发一个MatchError异常。

解释:

使用引用操作符^来对现有变量的值进行模式匹配。在上面的Elixir示例中,变量new_password绑定到元组(Elixir数据结构用{}表示)的第一个项,而不是重新绑定current_password变量,我们对其现有值进行模式匹配。

现在,这个Elixir文档中的示例应该就有意义了。

iex(1)> x = 1
1
iex(2)> ^x = 1 # Matches previous value 1
1
iex(3)> ^x = 2 # Does not match previous value 
** (MatchError) no match of right hand side value: 2

应该将 ^current_passwordsecret-1 进行比较吗? { new_password, ^current_password } = { 'secret-2', 'secret-1'} - Snake Sanders
是的,你说得对。然而,这个例子是这样写的,以便发生MatchError错误。基本上,它是JavaScript上面例子的重写。 - theterminalguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接