19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-07-24

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の変数バインディングについて

※ ここで言う「バインディング」というのは、「何の値でどの変数を束縛しているかの情報」程度の意味です。用語の使い方はあまり正確ではないかもしれません……。

※ 平成31年4月29日: 記述が古くなっていたので修正しました。

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 を使って明示的に渡すようにしなさいよ、という理由でこうなってるらしいです。

以下の記述は Elixir 1.6 までの説明となります。

最後は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

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

Elixir での case 式や if 式内での変数代入について。

上記の挙動はバグだったらしいです。

バージョン 1.7 の Elixir で上のバグは修正され、変数のスコープは式の中に閉じるようになっています。
(以下、 Elixir 1.8.1 で試しています。)

iex(1)> x = 42
42
iex(2)> if true do
...(2)>   x = 99
...(2)> end
warning: variable "x" is unused (if the variable is not meant to be used, prefix it with an underscore)
  iex:3

99
iex(3)> x
42
iex(1)> x = 42
42
iex(2)> case 99 do
...(2)>   x -> x = x
...(2)> end
warning: variable "x" is unused (if the variable is not meant to be used, prefix it with an underscore)
  iex:3

99
iex(3)> x
42

if 式や case 式の中で宣言した変数はその式の中でのみ有効であり、外側のバインディングには影響を及ぼしていないことがわかります。

参考

19
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?