Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

Immutableとはなんぞや?なにが嬉しいの?

この記事では扱うImmutableの意味は以下の通りです。

  • ある値がimmutableであるとは、値が変更されない(できない)こと

constによる定数と、Immutableであるとは全くの別物です。

wikiにも同様の記述があります(異なる界隈ではconstとimmutableを同一としている例もあるので、文脈によって意味が変わってしまう点に留意してください)。

実はImmutableなオブジェクトは身近に存在している

「Immutableって不便じゃない?」と勘違いしておりませんか?実はStringやNumberだってImmutableです。文字列が勝手に書きかわることはありません*1。もしもMutableだったら、関数に渡すのも神経を使わなければなりません。

// もし文字列がMutableだったら…
const str = "Hello World!";
evilFunction(str);
assert.equal(str, 'Goodbye World!'); // こんなことが起きてしまうかもしれない!

ArrayはMutableなので、再代入せずに変更可能です。例えばsort()メソッドを使うと値が書き換わってしまいます。

const arr = [1, 2, 3];
arr.push(4); // arrの指す配列が変化している
evilFunction(arr);
assert.equal(arr, [4, 3, 2, 1]); // 十分にありえる

Stringのメソッドである.replace().slice()は呼び出し元のオブジェクト(文字列)を変えないで、新しく文字列を返しています。このように呼び出し元のオブジェクトに変更を加えないメソッドを非破壊的メソッドといいます。

それに対して、Arrayの.push().pop()のように、呼び出し元のオブジェクトに変化を加えるメソッドを破壊的メソッドといいます。

つまり、「すべてのメソッドが非破壊的なオブジェクトはImmutableである」と定義できます。

Immutableのメリット

Immutableであるとは、「その値に対して破壊的変更が起きない」を意味します。Immutableであると嬉しいポイントは2つあります。

  1. オブジェクトを関数に渡したり、メソッド呼び出しによって変更が生じない
  2. 新しいオブジェクトを作るとき、差分だけ保持して、残りは同じものを参照する省メモリな実装ができる

値が変わる心配がない

例えばある配列arrをがあります。これを未知の関数に渡すとします。

const arr = [1, 2, 3, 4, 5];
unknownFunc(arr);
arr; // 変化しているかもしれない

unnknownFunc()の中でarr.sort()arr.splice()を呼び出されてしまうとarrは変化してしまいます。arrが変更されているかどうかを知るには、関数の中身をすべて見なければなりません。デバッグをしていてarrが変わっていたとき、どこで変更されたのか調べる範囲が広くなってしまいます。配列ならまだしも、文字列が勝手に変更されてしまうようでは不安でなりません。

Immutableであれば、破壊的変更がされないと保証されるので、関数に渡しても心配ありません。

省メモリに変更履歴を管理できる

参考:「オブジェクトをイミュータブルにしろ」って言うけど、それってたとえば状態が変わったらオブジェクト作り直すってことでしょ、ちょう非効率じゃん。って思ってたんだけど、 - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く

理屈の上ではわかりますが、技術的に僕には実装できないので、概念だけ説明します。

たとえばある巨大な配列(イミュータブルであるとする)を考えます。この配列の変更履歴を取りたいとします。このとき、この巨大配列と、そのあとの配列をそのまま保存しようとするとムダが発生していると理解いただけると思います。

const BigArray = new ImmutableArray([/*...*/]);
const BigArray2 = new ImmutableArray(BigArray.concat(3));
const history = [BigArray, BigArray2];

Immutableであれば、差分管理で効率よくメモリを使えます。つまり、「BigArrayの内容をコピーせず、変更した部分以外はBigArrayを指す」ように実装すればBigArray分のメモリ消費だけでよくなります。

f:id:panda_noir:20190315182722p:plain

実際にImmutableを使ったコード例

実際に例を見てみます。今回はImmutableJSを使います。

const _ = require('immutable');
let history = _.List(['http://hoge.com', 'http://fuga.com']); // イミュータブルな配列
let index = 0;
const open = (link) => {
    // リンクを開く処理をする関数
    // ほかにも色々やってて結構長い
}
const back = () => {
    index -= 1;
    if (index < 0) index = 0;
    open(history.get(index - 1));
    // さらにら色々な処理
}
open('http://piyo.com');

Immutableなら配列に要素を追加できない?

「Immutableな履歴なんてどう実装するんだ?」と疑問になるでしょう。

history、つまり履歴はこれまでの経路を保持してなければいけません。つまり、Mutableでなければならないのです。しかし、思い出してください。constとImmutableであるとは別物です。だから、変数に新しいオブジェクトを再代入すれば良いのです。これは全然構いません。オブジェクトに変更がなければ良いのです。

const addHistory = (link) => {
    // history.push(link)はhistoryにlinkを加えた新しいリストを返します。
    // pushはhistoryにlinkを追加した新しいListを返します。history自体に変更は加えません。
    return history.slice(0, now).push(link); // 戻るを押した後で新しいリンク開いたら、今より後ろの履歴は消える
}
open(link);
history = addHistory(link);

こんな感じです。これなら「historyが変更されている=再代入されている」箇所を見ていくだけでhistoryの状態遷移をたどれます。簡単ですね。

終わりに

以上はネットで拾い読みして経験で補完したものなので間違いが含まれているかもしれません。まあ信用しないで「こんなものか」みたいに聞き流してもらえればいいです

*1:現在のRubyはデフォルトでは文字列はmutableだそうです