Panda Noir

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

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

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

  • 値が変更されない(できない)ことを「ある値がimmutableである」という

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

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

StringもImmutable

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

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

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

let str = 'hoge';
str += 'fuga'; // 'hoge'と'fuga'を足した新しいオブジェクトが代入されるだけで、'hoge'に変更を加えたわけでない
str.replace('hoge', 'fuga'); // 新しい値を返してはいるが、文字列自体を変更しているわけではない

let arr = [1];
arr.push(2); // [1]に2を追加し、配列自体が変更されている

大きすぎるメリット

Immutableであるということは、その値に対して破壊的変更が起きないということです。これによって嬉しいことは2つあります。

  1. 状態について知りたいとき、いちいちそれを渡す関数、メソッドの実装を見なくて済む
  2. 新しいオブジェクトを作るとき、差分のみ保持し、残りは同じものを参照するという省メモリな実装ができる

例えばある配列arrを持っていたとします。これを未知の関数に渡すとします。

const arr = [1, 2, 3, 4, 5];
unknownFunc(arr)

このとき、arrは.sortされるかもしれませんし、.spliceされるかもしれません。あるいは.length=0にされてしまうかもしれません。つまりarrが変更されるかどうか知るには、関数の中身をすべて見なければなりません。デバッグしていてarrが変更されていたとき、どこで変更されたのか調べる範囲が広くなってしまうということです。

Immutableであれば破壊的変更がされないことが保証されているので関数に渡しても安心です。これがメリットその1です。

メリット2は詳しい解説は省略します。Immutable.jsでは差分のみを保持するという実装ではありませんが、拙作のライブラリimmutablify.jsではこのようなImmutableを実現しております。

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

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

3年後

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

この時、もしImmutableでないオブジェクトをhistoryに渡していた場合、historyの状態を調べるにはopen()がhistoryを変更するかどうかわざわざopen()の全文を読まなければなりません

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

ここでImmutableが活きてきます。historyがImmutableならば、open()はhistoryに変更を加えることができません。

Immutableゆえに

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

つまり、ある値の状態を追うために関数まで潜らず済みます。

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

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

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

function addHistory(link) {
    // history.push(link)はhistoryにlinkを加えた新しいリストを返します。
    // あくまでhistoryはImmutableJSのリストであり、pushによって変更されていないことに注意してください。
    return history.slice(0, now).push(link); // 戻るを押した後で新しいリンク開いたら、今より後ろの履歴は消える
}
open(link);
history = addHistory(link);

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

終わりに

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