Panda Noir

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

イミュータブルを簡単に実現するライブラリ「Immer」

なかなかクールなライブラリだったので紹介します。

今までのライブラリと何が違うのか

今までのイミュータブルライブラリといえばImmutable.jsMoriがあります。これらは「イミュータブルな配列」や「イミュータブルなMap」を提供するライブラリです。独自の配列やMapなので、通常の配列やオブジェクトのような書き方ができません。

const { List } = require('immutable');
const list = List([1, 2, 3]);
const arr = [1, 2, 3];
list.get(0); // c.f. arr[0];

arr[0]obj.keyのような書き方でアクセスできません。独自の書き方を強制されるのはかなりのストレスです。

それに対し、Immerは「JavaScriptのオブジェクトへのイミュータブルな操作」を提供します。

const produce = require('immer').default;
const arr = [1, 2, 3];
const pushedArr = produce(arr, draft => {
    draft.push(4, 5, 6);
    draft[0] = 0;
});
console.log(arr); // [1, 2, 3]
console.log(pushedArr); // [0, 2, 3, 4, 5, 6]

ご覧のように、元のオブジェクトは変更されず、返り値は破壊的変更を加えたものとなります。

返り値はただの配列なので、当然普通の配列として扱うことができます*1

何が嬉しいか

まず、扱う対象が普通のオブジェクトであり、返されるものも普通のオブジェクトのため通常のJavaScriptの書き方ができます

また、produce()の中の記述は破壊的変更が可能なので、可読性が高いです。

さらに、独自の配列を追加したり独自のAPIを強制しないので、今までのコードに組み込むことも容易です。

試しにイミュータブル版メソッドを追加する

const produce = require('immer').default;
const immer = {
    push: Symbol('push'),
    pop: Symbol('pop')
};
Array.prototype[immer.push] = function(...args) {
    return produce(this, draft => {
        draft.push(...args);
    });
};
Array.prototype[immer.pop] = function() {
    return produce(this, draft => {
        draft.pop();
    });
};
const arr = [1, 2, 3];
console.log(`push: ${arr[immer.push](4, 5, 6)}`);
console.log(`pop: ${arr[immer.pop]()}`);
console.log(`original: ${arr}`);

欠点

まだ開発途上のため、配列とプレーンオブジェクトしか扱えません。例えばDate()new MyClass()ではうまく動作しません。MapSetも無理です。今後に期待ですね

*1:注: ただし、freeze済みのオブジェクトが返されますので、破壊的変更を加えることはできません。設定でfreezeしないようにもできます