Panda Noir

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

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

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

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

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

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

実はImmutableなオブジェクトはすでに触っている

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

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

let str = 'hoge';
str + 'fuga'; // str自体に変化はない
str.replace('hoge', 'fuga'); // こっちも同様
str = str + 'piyo'; // strに+演算子によって新しく生成された文字列を代入しているだけ。'hoge'に変化を加えているわけではない

const arr = [1, 2, 3];
arr.push(4); // arrの指す配列が変化している

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

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

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

メリット: 値が変わる心配がない

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

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

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

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

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

arrならまだしも、Stringが勝手に変更されてしまうようでは不安でなりませんよね?

const str = 'hoge'; // もしStringがMutableだったら…
unknownFunc(str); // unknownFunc()の中で破壊的変更が行われてしまい…
console.log(str); // 'fuga'になってしまっていた!

このようなことが起きるかもしれません。いちいちこんな心配しながらプログラミングしたくありませんよね。

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

メリット2は詳しい解説は省略します。Immutable.jsでは差分のみを保持するという実装ではありませんが、拙作のライブラリimmutablify.jsではこのような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の状態遷移をたどれます。簡単ですね。

終わりに

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