イミュータブルが大事な理由、そしてImmerで簡単実現!

6 min read読了の目安(約5900字

始め

JS,特にReactを勉強してるとよくimmutabilityという言葉を聞きます。最近immerを使ってみて不意にimmutabilityは何で重要だったっけ?と思ったので、投稿します。


1. immutabilityとは

immutabilityは不変性、つまり変わらない性質という英単語です。

プログラミングでのimmutabilityは「stateを変更しないこと」とも言えます。(韓国では漢字の方の「不変性」を採用していますが、日本語あまり分からないので今回は英単語そのまま書きます。)

ここで、「stateを変更する」ということは正確に何を意味するのでしょうか?一番簡単な例を見てみましょう。

let greeting = "おはよう"
greeting = "さよなら"

このように再代入したら変数の値が変わるので、立派な「stateを変更する」行為です。だったら再代入しなければimmutability守れるんだ!と思うかもしれませんが、違います

次の例です。

const string = "hello";
const makeUpperCase = (string) => string.toUpperCase();

console.log(makeUpperCase(string)); //HELLO
console.log(string); //hello

を引数でもらったstringを変更して返す関数です。特に問題はなさそうです

const array = ["おはよう", "hello"];
const addArray = (array) => {
  array.unshift("안녕");
  return array;
};

console.log(addArray(array)); // ["안녕", "おはよう", "hello"]
console.log(array); // ["안녕", "おはよう", "hello"]

問題はこれです。引数でもらった配列に新しい要素を追加して返す関数ですが、どこでも再代入はしてないのにarrayの値が変更されてしまいました。その理由について説明します。

関数に引数として変数を渡す時は、その変数のコピー本を渡します。

ここで、その変数の中身がプリミティブタイプなら値を新しいメモリ保存して渡します。ですので、コピー本を変更しても原本には影響がありません。

しかし、変数の中身がオブジェクトタイプならその変数のメモリアドレスを渡します。原本とコピー本が同じメモリアドレスを共有するので、コピー本を変更したら原本にも影響がでるのです。

(Javascriptのコピーメカニズムについては以前書いた「シャローコピー・ディープコピーとは」でも説明してますので、ご参考ください。)

このようにstateを変更する行為は再代入だけではありません。ですので、「stateを変更する」ということはメモリに保存されている値を変更するすべての行為と定義したほうが良いと思います。

2. immutabilityが大事な理由

2-1. 予想できないstateの更新を塞げられる

stateはアプリケーションの現状を示してくれるものです。しかし、あちこちでstateを参照したり変更しまくった場合はアプリケーションの現状がわからなくなる危険があります。

すごく極端的な例を上げてみます。

const hi = { greeting: 'おはよう' };

// コード1万行

console.log(`${hi.greeting}, みんちゃん`);

ここでconsole窓見たらおはよう, みんちゃんが出ると思います。が、誰かコード1万行の中でhiオブジェクトを変更してしまったら急にさよなら, みんちゃんとかが出る可能性も全然あります。

この状況で誰がhiオブジェクトを更新したのか探すことは難しいです。(しかもバグじゃないからエラーも出ない)

immutabilityを守るということはメモリに保存されているデータを変更しないことなので、こういう予想できないstateの更新を塞げられます

2-2. state変更が追跡できる

上のサンプルコードですが、実はコード1万行のなかにこういう関数があるとします。

const hi = { greeting: "おはよう" };

function changeName(object) {
  object.greeting = "さよなら";
  return object;
}

const bye = changeName(hi);

console.log(`${hi.greeting}, みんちゃん`); // さよなら, みんちゃん
console.log(`${bye.greeting}, みんちゃん`); // さよなら, みんちゃん

ここで原本オブジェクトのプロパティまで変更されるのも問題ですが、もう一つの問題はこういうstateの変更を追跡できないということです。この例のhiオブジェクトとbyeオブジェクトは変数名が違うだけで、実際は同じメモリに保存されてるデータです。

ですので、Javascriptは2つを同じものだと判断します。

console.log(hi === bye) // true

しかしオブジェクトと配列などを全く変更しないことも不可能でしょう。この問題は新しいオブジェクトや配列を生成することで解決できます。

const hi = { greeting: "おはよう" };

function changeName(object) {
  const newObject = { ...object }; // オブジェクトコピー
  newObject.greeting = "さよなら";
  return newObject;
}

const bye = changeName(hi);

console.log(`${hi.greeting}, みんちゃん`); // おはよう, みんちゃん
console.log(`${bye.greeting}, みんちゃん`); // さよなら, みんちゃん

このようにオブジェクトをコピーしてそれをいじったら大丈夫でした。この書き方がめんどうくさいなら以下のような書き方でもいけます。

function changeName(object) {
  return {
    ...object,
    greeting: "さよなら"
  };
}

changeNameが返したオブジェクトはhiと全然違うオブジェクトです。予想できないオブジェクトの変更も塞げられますし、stateの変更も追跡できるようになりました。

console.log(hi === bye) // false

オブジェクトを更新させるとき「更新されたオブジェクト」を新しく生成すると、更新前のオブジェクトと更新後のオブジェクトを比べた時falseがでます。この事実を利用してオブジェクトが更新されたとわかります。

これがReactがimmutabilityを大事にする理由です。Reactはstateの変更を探知し、変更前と変更後のオブジェクトを比較して違うと判断したらコンポーネントを再レンダリングします。この比較するときにメモリアドレスが違ってたらfalseが出るのですぐ判断できます

もしimmutabilityを守らなかったならすべての要素を一つ一つ比較することになるので、もっとコストがかかるでしょう。

3. immutabilityはめんどうくさい

しかし残念ながらimmutabilityを守ることはめんどうくさいです。

これは私が勉強で書いてたコードです。SNSを実装してて、コメントを追加する機能をreduxのreducerに書いてました。

const reducer = (state, action) => {
  switch (action.type) {
    // 色々ある
    case ADD_COMMENT_SUCCESS: {
      const postIndex = state.mainPosts.findIndex((mainPost) => mainPost.id === action.data.postId);
      const commentAddedPost = { ...state.mainPosts[postIndex] };
      commentAddedPost.Comments = [generateComment(action.data.content), ...mainPost.Comments];
      const mainPosts = [...state.mainPosts];
      mainPosts[postIndex] = commentAddedPost;
      return {
        ...state,
        mainPosts,
        addCommentLoading: false,
        addCommentDone: true
      };
    }
    // 色々ある
    default:
      return state;
  }
};

配列とオブジェクトをずっとコピーしてるから...も多いし、長いし、読みづらいしとりあえずめんどうくさいです。これ書いてマジかと思いました。

ですので、immerというライブラリを紹介します。

4. Immer

4-1. Immerとは

immerはimmutabilityを守るコードを楽に書けるようにしてくれるライブラリです。

使い方は公式サイトのQuick Exampleを見るとすぐわかります。

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

immerが提供するproduce関数さえ使えば簡単にimmutabilityを守れます。
produce関数は2つの引数が必要で、第1引数は更新したいオブジェクトもしくは配列、第2引数は第1引数をどう更新するかを定義する関数です。

4-2. Immerを使うと

3の例をimmerを使ってリファクタリングするとこうなります。

const reducer = (state, action) =>
  produce(state, (draft) => {
    switch (action.type) {
      // 色々ある
      case ADD_COMMENT_SUCCESS: {
        const commentAddedPost = draft.mainPosts.find((mainPost) => mainPost.id === action.data.postId);
        commentAddedPost.Comments.unshift(generateComment(action.data.content));
        draft.addCommentLoading = false;
        draft.addCommentDone = true;
        break;
      }
      // 色々ある
      default:
        break;
    }
  });

produceの第2引数である関数の中にロジックを書きます。書く時、immutabilityなんて思いっきり破壊すればよいです。そうしたらimmerが勝手にそのコードをimmutabilityを守るコードに変えてくれるので、めちゃくちゃ便利です。ちょっと感動しました。


終わり

調べたら前はimmutable.jsというライブラリがよく使われてたようですが、npmtrendsで確認したら最近はimmerが勝ってました。immerのほうが使いやすいからだと思います。