概要
Elixir の大きな特徴の一つである「不変性」について解説している章。
Elixir では(束縛し直さない限り)常に変数は同じ値を参照する。そのため、オブジェクト指向言語のように、このメソッドは変数自体を書き換えるのか?コピーを返すのか?と悩む必要がなく、可読性が高いコードを実装することができる。
不変性
不変性とは
例えば
ids = [1, 2, 3]
do_something_with(ids)
print(ids)
こんな感じで、「最初に id のリストを渡して、do_something_with
で何らかの処理を行う。最後にデバック用に、どんな id を渡したか出力する」という実装が存在した場合、最後の print
では 1, 2, 3
が出力されることを期待してまう。
しかし、例えば do_something_with
が以下のような実装なら (Ruby)
def do_something_with(a)
a.delete(2)
end
ids = [1, 2, 3]
do_something_with(ids)
print(ids)
#=> [1, 3]
途中 2 が消されて ids の値が変わってしまっている。
こういう具合に、多くのオブジェクト指向言語では簡単に期待を覆す実装ができてしまう。
それに対して、Elixir は常に「不変」で、メソッドは引数のコピーを作成して返すので、これが「破壊的」かどうかについて頭を悩ませる必要がない。これは、可読性を上げて、並行性の怖さを軽減してくれる。
不変性の性能
Elixir はデータを更新するごとに毎回新しいデータのコピーを作るので、そこら中にゴミが残ってしまい、とても非効率に見えるが、実際は逆である。
不変であるからこそ、新しいデータを作成する際に、前のデータを再利用することができる。
> list1 = [3, 2, 1]
[3, 2, 1]
> list2 = [4 | list1]
[4, 3, 2, 1]
多くの言語では list2 を作成する際は、4, 3, 2, 1
が入った新しいリストを作成する。これは list1
が「可変」だからである。
しかし、Elixir は list1
が「不変」であることを知っているので、単に「先頭に 4、末尾に list 1」を持つリストを作ればよい。
また、多くの言語と同じようにメモリを解放するために Elixir もガーベージコレクタを備えている。
このガーベージコレクタは性能に悪影響を及ぼす場合があるが、Elixir は各プロセスが個別のヒープを持っているため、全てのデータを一つのヒープで扱う言語に比べ、各ヒープがとても小さい。
そのため、ガーベージコレクションは高速で、その上ヒープが一杯になる前にプロセスが終了すれば、ガーベージコレクションを使うことなくメモリが解放される。
上記の様に不変性による性能への影響は心配する必要がない。