José Valimさん1の2016年1月12日付のブログ記事"Comparing Elixir and Erlang variables"の翻訳です。
時々Erlangのプログラマは「Elixirの変数は隠れたバグの温床になるのでは」と心配するようです。この記事ではそういう心配についてと、Erlangでの変数も「隠れたバグ」を生み出しかねず、一方でElixirはそれらのうちのいくつかを除去するということについて議論します。
話を始める前に短い放棄声明を…: Elixirは可変(mutable)な変数を持っていません。あるのは再バインドです。可変性はよくストレージと関連付けられます。Elixirでは保存された値を変更することはできません(Erlangと同じです)。可変な変数の例としてF#を見てみましょう。F#では明示的にmutable
キーワード(例えばlet mutable x = 5
)によりインナーループの内側の変数を変更することができるようになり(Enum.map
の内側やリスト内包表記と等価です)、ループを抜けた後も変更された状態を見ることができます。これが可変性で、ElixirやErlangではプロセスやETS3と言ったストレージを明確に使わないかぎり不可能なことなのです。
話を基に戻して。この記事ではコードを変更した時の隠れたバグの可能性について調査します。これらのバグはErlang及びElixirの変数が暗黙の挙動を持っているために起きます。Elixirの再バインドは暗黙的に行われますし、Erlangのパターンマッチも暗黙的に行われます。これらのバグはプログラマが変数を追加したり削除したりする際にそのコンテキストを十分に意識していないと発生することがあります。
ではいくつか例を見てみましょう。次のようなElixirコードを考えてみてください:
foo_bar = ...
# 何らかのコード
use_foo_bar(foo_bar)
さてここでfoo_bar
が上記のスニペットより前に現れていたら何が起きるでしょうか。
foo_bar = ... # 新しく追加された行
foo_bar = ...
# 何らかのコード
use_foo_bar(foo_bar)
このコードはコンパイラは新しく追加されたfoo_bar
が使われないままだという警告を出すでしょうが4正しく動作するでしょう。しかし、もし新しく追加された行がfoo_bar
より後に現れていたら何が起きるでしょうか。
foo_bar = ...
# 何らかのコード
foo_bar = ... # 新しく追加された行
use_foo_bar(foo_bar)
もしuse_foo_bar
関数を最初の変数foo_bar
の値で使うつもりだったとしたらこれでは意味合いが変わってきてしまいます。まったく、不注意な変更はバグを呼ぶものです。
ではErlangを調べてみましょう。コードは:
FooBar = ...
% 何らかのコード
use_foo_bar(FooBar)
定義より前にFooBar
が現れたら?
FooBar = ... % 新しく追加された行
FooBar = ... % ここの、元からあった行はエラーになる
% 何らかのコード
use_foo_bar(FooBar)
Erlangのこのコードは何も言わずに継続されることなく実行時にクラッシュします5。確かによりよいかもしれませんがErlangで変数を使う際にはその変数が以降マッチされないことを保証しなければなりません。この場合FooBar
は二度と割り当てもされずマッチもされないことを、です。
では定義の後にこの行を置いたら?
FooBar = ...
% 何らかのコード
FooBar = ... % 新しく追加された行はエラーになる
use_foo_bar(FooBar)
今度は新しく追加された行がクラッシュします。言い換えるとErlangの暗黙のマッチングにより変数を使った後はコード内を全く心配しなくてよいということです。しかし、使う前については十分に注意しなければなりません。先に書かれたコードが先々の変数と暗黙にマッチできてしまうからです。
別の言い方をすればElixirはある変数を使った後のコードに注意が必要で、Erlangはある変数を使う前のコードに注意が必要です。Erlangでは変数を使うより前の全てのコードについてあなたが知っていなければなりません。Erlangの良い点のひとつはそのマッチで明示的にコードがクラッシュするということです。
しかし、case式を考えるとことはさらに複雑になっていきます。
Case式
新しい変数をcase式の中でマッチさせたい。Elixirではこんな感じに書けます:
case some_expr() do
{:ok, safe_value} -> perform_something_safe()
_ -> perform_something_unsafe()
end
では、うっかりElixirで変数safe_value
をこのcase式より前に使ってしまったら。
safe_value = ... # 新しく追加された行
# 何らかのコード
case some_expr() do
{:ok, safe_value} -> perform_something_safe()
_ -> perform_something_unsafe()
end
何も起きません。コードは再バインドのためにうまく動作します。
ではErlangでは何が起きるでしょう:
case some_expr() of
{ok, SafeValue} -> perform_something_safe();
_ -> perform_something_unsafe()
end
さて、前に変数を置いたら何が起きるでしょう?
SafeValue = ... % 新しく追加された行
% 何らかのコード
case some_expr() of
{ok, SafeValue} -> perform_something_safe();
_ -> perform_something_unsafe()
end
さて、あなたは今知らないうちに危険な潜在バグをコードに入れたのです!繰り返しますが、Erlangは暗黙的にマッチするので、case文の最初の節ではSafeValue
は絶対にバインドされないのにマッチだけは成功してしまうためperforme_something_unsafe()の方が実行されてしまうかもしれません6。
Erlangにおいて同様のバグは既存の変数とマッチングさせているのにその変数を削除してしまった場合にも起きます7。では以下の様なElixirの動作するコードがあったとしてみてください:
safe_value = ...
# 何らかのコード
case some_expr() do
{:ok, ^safe_value} -> perform_something_safe()
_ -> perform_something_unsafe()
end
Elixirのパターンマッチは明示的なのでsafe_value
の定義を削除したとしたらこのコードはコンパイルが通りません8。ではErlangでちゃんと動くバージョンを見てみましょう。
SafeValue = ...
% 何らかのコード
case some_expr() of
{ok, SafeValue} -> perform_something_safe();
_ -> perform_something_unsafe()
end
ここで変数SafeValue
を削除したとするとcase文の最初の節はSafeValue
とマッチするのではなくバインドされるので、またも知らないうちにこのコードの挙動を変えてしまいました!どちらの場合についてもElixirのやり方なら防げるのにまたもバグです。
この点についてElixirでは:
- 変数を新しく使う場合はそれ以降のコードを全て分析する必要がある。ちゃんと分析できないとバグが出るかもしれない。
- 変数のマッチングは常に安全である。なぜなら変数の再バインドがあるし、明示的にマッチさせるためには^(ピン演算子)があるから。
一方Erlangは:
- 変数を新しく使う場合はそれより前、及びそれより後のコードを全て分析してそれがマッチングか割り当てかを明確にしておく必要がある。ちゃんと分析できないと実行時にクラッシュするだろう。
- 変数を新しく使う場合はそれ以降のコードを全て分析して後のcase文の意味を変えてしまわないか確認しておく必要がある。ちゃんと分析できないとバグが出るかもしれない。
- 変数を削除する場合はそれ以降のコードを全て分析してあとにあるcase文の意味を変えてしまわないか確認しておく必要がある。ちゃんと分析できないとバグが出るかもしれない。
通し番号付きの変数
最初にElixirで新しく変数foo_bar
を使うとその変数が後で使われている場合はコードの意味が変わってしまうと述べました。しかしそういうケースの大半は意図して行うものです。例えばElixirでは9:
foo_bar = step1()
foo_bar = step2(foo_bar)
foo_bar = step3(foo_bar)
# 何らかのコード
use_foo_bar(foo_bar)
Erlangで:は10
FooBar0 = step1(),
FooBar1 = step2(FooBar0),
FooBar2 = step3(FooBar1),
% 何らかのコード
use_foo_bar(FooBar2)
では新しくfoo_bar
(step_4
)をElixirのコードに入れるとすると?
foo_bar = step1()
foo_bar = step2(foo_bar)
foo_bar = step3(foo_bar)
foo_bar = step4(foo_bar) # 新しく追加された行
# 何らかのコード
use_foo_bar(foo_bar)
これで動きます。ではErlangは?
FooBar0 = step1(),
FooBar1 = step2(FooBar0),
FooBar2 = step3(FooBar1),
FooBar3 = step4(FooBar2),
% 何らかのコード
use_foo_bar(FooBar2) % 全てのFooBar2を書き換えないといけない
もし開発者が新しい変数を入れて以降のFooBar2
を書き換えるのを忘れるとプログラムの意味が変わってしまい、Elixirなら再バインドしたはずのバグを生じてしまいます。これは特にすべて変更した後にひとつだけ変数を変更し忘れていたような場合に厄介です。なぜならそのようなコードは「変数が使われていません」といった警告を出さないからです。また中間状態を追加(例えばstep2
とstep2
の間に)するとさらにエラーを生じがちになります。
通し番号付きの変数の方が後ろに書かれたコードでFooBar2
も
FooBar3
も使えるという利点があるではないかという人もいるでしょう。例えば:
FooBar0 = step1(),
FooBar1 = step2(FooBar0),
FooBar2 = step3(FooBar1),
FooBar3 = step4(FooBar2),
% 何らかのコード
use_foo_bar(FooBar2),
something_else(FooBar3)
しかし私は上記のコードはよくない実例だと考えます。なぜならばFooBar2
という名前にはFooBar3
とは違うという事情を示すためのヒントが何も含まれていないからです。この場合だとコードの一部がどうして他の名前を使わずにこの名前を使っているのかについて変数名は全く反映できていません。明確な意味を持つ名前を付けるほうが通し番号付きの変数よりチームメンバーにとっては遥かにマシでしょう。
まとめ
ElixirとErlangの変数はそれぞれ再バインドとパターンマッチという暗黙の挙動を提供しているので既存のコードについて変数を追加したり削除したりする場合には注意が必要です。そのため、Elixirが隠れたバグの温床になるような場合はErlangでも同じようなバグを違った状況下で発生させることを見てきました。それだけではなくErlangは新しい変数を追加した場合そこより前だけではなく、後ろについても知識を持っていないといけません。Elixirなら後ろについてだけ知識が必要です。双方の言語でこれらのバグを回避するには変数の再バインドまたはパターンマッチ操作を明示的に行うかあるいはどちらも禁止するしかありませんが、どちらの言語もそれはできません。
この記事を読んで「僕のコードには関係ないや」という反応をする人もいるかもしれませんが実のところどんな小さな関数でもこれは起きているのです:
- http://erlang.org/pipermail/erlang-questions/2010-January/048742.html
- http://erlang.org/pipermail/erlang-questions/2010-January/048762.html
- http://erlang.org/pipermail/erlang-questions/2010-January/048767.html
一方でErlangやElixirでプログラムを書くともっとバグが出やすいということではありません。結局のところ、Erlangの開発者たちは何十年にもわたって堅牢なコードを書いてきました。こういう「変な癖」はどんな言語にも存在し、プログラマたちが経験を積むにつれ最終的にはそれらは自家薬籠中の物となります。それはまさに「僕のコードには関係ないや」という言葉が出る所以です。
最後に、どんな言語でも文脈に注意を向けないままだと安全なコード変更を保証してはくれないでしょう。常に「隠れたバグ」が起こりえます。例えばClojureやJavaSctiptやRubyと言った言語であっても、変数及び関数の名前は同じ名前空間の中にあり新しく変数を使う場合は関数呼出しの意味を変えてしまうかもしれません11。ErlangもElixirも変数用にひとつ、関数用にひとつ、計2つの名前空間を持っているのである種の「隠れたバグ」からは守られています。
さらに型システムやコンパイラの警告、テストスイートは全てこれらの問題を解決するためのテクニックです。言語によってはElixirのパイプライン演算子(|>
)のように繰り返しのコードをさらに読みやすくエラーが起きにくい形に書き換えるのを手助けするパターンを提供しています。
少なくともこの記事によってElixirの変数はErlangのより安全ではない(またはその逆)と言ったクレームにケリが付いてくれることを期待しています。
謝辞:Joe Armstrong, Saša Juric, James Fish, Chris McCord, Bryan Hunter, Sean Cribbs及びAnthony Ramineにはこの記事をレビューしてもらいフィードバックをいただきました12。
-
言わずと知れたElixir作者。
昔Erlangの変数について勉強したとき、再代入不可の変数って定数?、どうやって使うの?2と思ったものです。確かに一度代入したものが変更できないのであればプログラムの見通しはよくなります。一方でElixirの変数は一見すると再代入可能に見えます。再代入禁止の方がバグが混入しにくい?本当?
この辺りについてジョゼ自ら説明します。 ↩ -
代入された結果を使い回す以外に、引数や再帰呼び出しした先の変数を使って状態を受け渡すことで見た目状態変化させるように使いますね。 ↩
-
Erlang Term Storage、Erlang用のインメモリーデータベースです。 ↩
-
warning: variable foo_bar is unused
と表示されます。 ↩ -
例えば
** exception error: no match of right hand side value...
というエラー。 ↩ -
この説明だと少しわかりにくいのですがsome_expr()が正常終了した場合に{ok, "何らかの値"}を返すとすると、その場合SafeValueがそれまでに使われていなければSafeValueには"何らかの値"が入りperform_something_safe()が実行されることになります。もし先にSafeValueに先に値をバインドしてしまっていたとすると、このcase文はsome_expr()がどんな値を返しても、バインドしたのがたまたま"何らかの値"の場合以外では必ず失敗して_節の方が実行されます。意図した動作と全く異なる動作なので原因に気づきにくいでしょうね。 ↩
-
今と逆のケースですね。 ↩
-
case文の最初の節で^(ピン演算子)で再バインド禁止してますからね。** (CompileError) nantoka.ex:行番号: unbound variable ^safe_valueみたいなエラーが出てしまいます。 ↩
-
こういう場合は普通はみんな大好きパイプライン演算子(|>)を使うと思いますが… ↩
-
Erlangは変数の再バインドというか異なる値とのパターンマッチを禁止していますからこのように通し番号でもつけて別の変数を使わないといけません。 ↩
-
ClojureはLisp-1なので変数と関数は同じ名前空間にあります。 ↩
-
なにげにすごいメンバー… ↩