Erlang
Elixir

Elixirでも変数の再代入はできないよ

Elixirの記事ですが、まずErlangの話から始めさせてください。

マッチ演算子

Erlangの特徴の一つに、「 = はパターンマッチを行う演算子である」というものがあります。

1> Int = 42. % 未束縛の変数を使ったパターンマッチ。
42
2> Int = 42. % 束縛済みの変数に対するパターンマッチなので、値を比較する。成功。
42
3> Int = 99. % 失敗。
** exception error: no match of right hand side value 99

Erlangでは以下のような書き方ができます。

1> Int = 1.
1
2> 1 = Int.
1
3> 42 = Int.
** exception error: no match of right hand side value 1

= 演算子を、他の多くの言語のように代入の為の演算子だと考えていると、上の挙動にぎょっとするかもしれませんが、Erlangでは = はあくまで「マッチ演算子」なのです。

つまり、

Int = 1.
...

というのは、

case 1 of
  Int -> ...
end.

と書くのと同じような事です(多分……)。

Haskellとかだと変数のシャドウイングが行われるので、パターンマッチ式の外と中で同じ変数を使う事ができますが、

Prelude> :{
Prelude| test =
Prelude|   let x = 42 in
Prelude|   case 99 of
Prelude|     x -> x
Prelude| :}
Prelude> test
99

Erlangだと変数に束縛されている値を見て、それと比較してくれます。

1> X = 42.
42
2> case 99 of
2>   X -> X
2> end.
** exception error: no case clause matching 99
3> case 99 of
3>   Y -> Y
3> end.
99

なので、同じ名前の変数に束縛できないんですね。

ちなみに、Erlangでも無名関数の引数の場合はシャドウイングが行われるようです。

1> X = 42.
42
2> Y = 99.
99
3> F = fun(X) -> X + Y end.
#Fun<erl_eval.6.99386804>
4> F(99).
198

Elixirの場合

Elixirでも、 =マッチ演算子です。

iex(1)> i = 42
42
iex(2)> 42 = i
42
iex(3)> 99 = i
** (MatchError) no match of right hand side value: 42

しかしElixirのマッチ演算子はErlangと違って、シャドウイングを行います。

iex(1)> i = 42
42
iex(2)> i
42
iex(3)> i = 99
99
iex(4)> i
99

これを見て、「Elixirは変数の再代入が行える」と言われる事もありますが、既に述べた通り、これは再代入しているわけではなく、シャドウイング、つまり 同じ名前の変数を再度定義している だけです。
謂わば再束縛ですね。

再代入では無い事は簡単に確かめられます。
再代入が可能で、Elixirに似ている言語、Rubyと比較してみましょう。

irb(main):001:0> i = 42
=> 42
irb(main):002:0> f = -> { i }
=> #<Proc:0x007fbeea8334d8@(irb):2 (lambda)>
irb(main):003:0> f.()
=> 42
irb(main):004:0> i = 99
=> 99
irb(main):005:0> f.()
=> 99

変数に再代入を行うと、同じ変数を参照している全ての場所で、その変数の値が書き換わって見えます。
再代入とはそういうものなので当然です。

これはPythonでも、

>>> i = 42
>>> i
42
>>> f = lambda : i
>>> f()
42
>>> i = 99
>>> i
99
>>> f()
99

JavaScriptでも同じです。

> let i = 42
undefined
> i
42
> const f = () => i
undefined
> f()
42
> i = 99
99
> f()
99

一方、Elixirで似たような事をすると、

iex(1)> i = 42
42
iex(2)> f = fn -> i end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(3)> f.()
42
iex(4)> i = 99
99
iex(5)> f.()
42

i = 99 で変数iを書き換えても、クロージャの中では元のiを参照し続けています。
再束縛が、既存の変数の参照には何の変化も及ぼしていない事が分かります。

そういうわけで、Elixirでも変数の再代入はできないのです。
あくまで、「同じ名前の変数を新しく作っている」だけなのです。

おまけ: Elixirの変数バインディングについて

Elixirの変数のバインディングは結構おもしろ挙動をします。

無名関数はネストしたスコープを持ちます。外側にアクセスでき、書き換えはできない。
これはわかりやすいですね。

iex(1)> i = 42
42
iex(2)> f = fn ->
...(2)>   j = 99
...(2)>   i + j
...(2)> end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(3)> f.()
141
iex(4)> j
warning: variable "j" does not exist and is being expanded to "j()", please use parentheses to remove the ambiguity or change the variable name
  iex:4

** (CompileError) iex:4: undefined function j/0

名前付き関数は独自の(外側にアクセスできない)バインディングを持ちます。

iex(1)> defmodule Test do
...(1)>   x = 42
...(1)>   def test do
...(1)>     y = 99
...(1)>     x + y
...(1)>   end
...(1)> end
warning: variable "x" is unused
  iex:2

warning: variable "x" does not exist and is being expanded to "x()", please use parentheses to remove the ambiguity or change the variable name
  iex:5

** (CompileError) iex:5: undefined function x/0

モジュール内の値を関数に渡したい場合は、 module attributes を使います。

iex(1)> defmodule Test do
...(1)>   @x 42
...(1)>   def test do
...(1)>     y = 99
...(1)>     @x + y
...(1)>   end
...(1)> end
{:module, Test, <<...>>, {:test, 0}}
iex(2)> Test.test()
141

この挙動は、名前付き関数が外の値にアクセスできてしまうと、コードの見通しが悪くなったり、意図しない挙動を誘発しやすくなったりするので、そういう事がしたいなら module attributes を使って明示的に渡すようにしなさいよ、という理由でこうなってるらしいです。

最後はcaseやifのような制御構文ですね。
制御構文は、外側のスコープにアクセスでき、かつ、バインディングを書き換える事ができます。

iex(1)> i = 42
42
iex(2)> if true do
...(2)>   i = 99
...(2)> end
99
iex(3)> i
99
iex(4)> case :ok do
...(4)>   :ok -> i = 0
...(4)> end
0
iex(5)> i
0

だたし、caseのパターンマッチで束縛された変数は、その節の中でだけ有効になります。

iex(1)> case 99 do
...(1)>   i -> i
...(1)> end
99
iex(2)> i
warning: variable "i" does not exist and is being expanded to "i()", please use parentheses to remove the ambiguity or change the variable name
  iex:2

** (CompileError) iex:2: undefined function i/0

なので、同じ名前の変数に値を束縛したとしても、外側のバインディングを書き換える事はしません。

iex(1)> x = 42
42
iex(2)> case 99 do
...(2)>   x -> x
...(2)> end
99
iex(3)> x
42

勿論、マッチ演算子で明示的にバインディングを書き換えると、外側のスコープにもそれが反映されます(上述)。

というわけで、

iex(1)> x = 42
42
iex(2)> case 99 do
...(2)>   x -> x = x
...(2)> end
99
iex(3)> x
99

みたいな書き方ができます。

参考