Panda Noir

JavaScript の限界を究めるブログです。

Immutableの利便性、大きなメリットについて。

この記事ではImmutableについて混乱を招かないように下のようにします。

  • オブジェクトの値が変更されないことをimmutableという
  • 変数は再代入可能。

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

wikiも同様のことが書いてあります。

StringもImmutable

Immutableって不便じゃない?と考えている人は勘違いしています。StringやNumberもImmutableです。文字列が代入された変数は再代入なしに勝手に書きかわることはありませんよね?

それに対しArrayはsort()メソッドなどのように再代入せずに変更可能です。それはArrayがMutableだからです。

ざっくりイメージができたかと思います。

let str = 'hoge';
str += 'fuga'; // 'hoge'と'fuga'を足した新しいオブジェクトが代入されるだけで、'hoge'に変更を加えたわけでない

let arr = [1];
arr.push(2); // [1]に2を追加し、変更を加えている

大きすぎるメリット

Immutableのメリットとしてはまず、破壊的変更が加えられないことが挙げられます。つまり、関数内、メソッド内でそのオブジェクトが変更されることがありません。

ただし、変数自体には再代入可能のため、オブジェクトに処理を加え、それを関数が返し代入というのはできます。文字列もsubstringなどのメソッドでは変更後の文字列を返すだけで変更はしませんよね。

Immutableで嬉しいことは二つあります。一つは状態について知りたいとき、いちいちそれを渡す関数、メソッドの実装を見なくて済むわけです。破壊的変更をされることはないですから。

もう一つは差分のみ保持し、残りは同じものを参照するという省メモリな実装ができることです。Immutable.jsではこの実装をしていませんが、拙作のライブラリimmutablify.jsではこのようなImmutableを実現しております。

例を出しましょう。今回はImmutableJSを使います。

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

3年後

コードを書いてから3年経ちました。あなたは久しぶりにこのコードを見て汚いコードを手直ししたくなりました。

この時、もしImmutableでないオブジェクトをhistoryに入れていたらあなたは「open()はhistoryを変更するんだっけ??見てみよう」といってopen()をわざわざ見なくてはなりません。

わざわざ長くて汚いコードを見なければなりません。

ここでImmutableが活きてきます。そう、open()はhistoryに変更を加えることができません。 あなたが「間違えた」開発をしていなければ。

Immutableゆえに

つまり、あなたはImmutableゆえにopen()関数という汚物をいちいち読む必要がないのです。実際のケースではopen()は更にサーバに接続する関数を呼んだり、それ受け取って処理する関数に投げたりします。だからopen()から呼び出した関数まで見なければいけないケースがほとんどです。

大きなメリットですね。historyの状態を追うために関数まで潜らず済むのですから。

historyとは履歴なり

「Immutableな履歴なんて無意味だろう」と混乱してこの記事に来たあなた、これから説明するのでご安心を。

historyつまり履歴は当然リンクした経路を保持してなければいけません。つまり、Mutableでなければならないのです。これが最大の混乱の原因です。 しかし、思い出してください。

  • 変数はmutable、再代入可能。

です。だから、変数に再代入をすれば良いのです。それは全然構いません。オブジェクトに変更がなければ良いのです。

関数またはメソッドでは様々変更を加えた新しいオブジェクトを返し、それを代入します。

function addHistory(link) {
    return history.slice(0, now).push(link); // 戻るを押した後で新しいリンク開いたら、今より後ろの履歴は消える
}
open(link);
history = addHistory(link);

こんな感じです。これならhistoryが変更されている=再代入されている箇所を見て回るだけで済みます。

終わりに

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