immutable.js を使ってみる

最近、ググっていると immutable.js を見かけることが多く、試してみることにしました。
immutable.js 自体は、github のタグを追うと 2014 年 には開発が始まっているものなので、新しいものではなかったです。

イミュータブル(immutable)だと何がいいんだろうか?ってことから調べてみました。

参考

イミュータブルだと何がうれしいのか

「イミュータブルだと、そもそも何がうれしいのか?」ということ調べてみた。
以下のようなコードを考えてみる。

1
2
3
4
5
6
7
const arr_a = [1, 2, 3, 4];
const arr_b = arr_a;

arr_b.push(5);

console.log(arr_a); // => [ 1, 2, 3, 4, 5 ]
console.log(arr_b); // => [ 1, 2, 3, 4, 5 ]

arr_bへの変更が、arr_aにも影響している。意図しない(してはいたとしても)こういうことを防げることが利点になる。

実は、スプレッド構文を使うと immutable.js を導入しなくても部分的には変更の影響範囲を制限できる。

1
2
3
4
5
6
7
8
9
10
11
const arr_c = [1, 2, 3, 4];
const arr_d = [...arr_c, ...[5, 6, 7, 8]];

console.log(arr_c); // => [ 1, 2, 3, 4 ]
console.log(arr_d); // => [ 1, 2, 3, 4, 5, 6, 7, 8 ]

const obj_e = { a: 1, b: 2, c: 3, d: 4 };
const obj_f = { ...obj_e, ...{ a: 11, b: 22, e: 7, f: 8 } };

console.log(obj_e); // => { a: 1, b: 2, c: 3, d: 4 }
console.log(obj_f); // => { a: 11, b: 22, c: 3, d: 4, e: 7, f: 8 }

...を先頭につけると、配列とオブジェクトを展開できる。
展開するとき、変更・追加したいものを付与すると別のオブジェクトになるので、元のオブジェクトに影響が無い。
変更追加はできたのだが、特定の項目削除はできないようだった。
スプレッド構文でイミュータブルなオブジェクトの扱いをするためにできるのは、Deep CopyDeep Copyを使った更新だと思っていいようだ。

immutable.js の導入

スプレッド構文でできること以外を immutable.js ではサポートしています。
immutable.js を導入します。

1
2
3
4
5
6
# 適当なディレクトリで以下を実行。
node -v
v12.12.0

npm init -y
npm i immutable

以降の確認は、src.jsを作成して確認します。

比較

配列

Immutable.js のImmutable.Listは、JavaScript の Arrayに該当するでしょう。

src.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//Array の場合
const arr1 = [1, 2, 3];
const arr2 = arr1;
arr2.push(4);

console.log(arr1); //=>[ 1, 2, 3, 4 ]
console.log(arr2); //=>[ 1, 2, 3, 4 ]
console.log(arr1 == arr2); //=>true
console.log(arr1 === arr2); //=>true

//Immutable.List の場合
const Immutable = require("immutable");
const list1 = Immutable.List([1, 2, 3]);
const list2 = list1;

console.log(list1.toJS()); //=> [ 1, 2, 3 ]
console.log(list2.toJS()); //=> [ 1, 2, 3 ]
console.log(list1 == list2); //=> true
console.log(list1 === list2); //=> true
// const list2 = list1の処理では、オブジェクトは同一

const list3 = list1.push(4); // #1
list3.push(5); // #2
list3.push(6); // #3
list3.push(7); // #4

console.log(list1.toJS()); //=> [ 1, 2, 3 ]
console.log(list3.toJS()); //=> [ 1, 2, 3, 4 ]
console.log(list1 == list3); //=> false
console.log(list1 === list3); //=> false
//変更を加えるメソッドは、常に新しいオブジェクトを作成する
//#2,3,4では、変更を加えるメソッドの結果を受け取っていないため、list3は#1 で示したlist1の末尾に4を追加した処理だけが反映されている

Immutable.jsでは、変更を加えるメソッドは新しいオブジェクトを返す。
なので、Array オブジェクトの末尾に追加する push などと同じメソッド名前を呼び出しても動作が異なる。
変更を加えた=違うオブジェクトなので、比較が容易。

オブジェクト

Immutable.js のImmutable.Mapは、JavaScript の Objectに該当するでしょう。

src.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//Object の場合
const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = obj1;
obj2.b = 2;

console.log(obj1.b); //=> 2
console.log(obj2.b); //=> 2
console.log(obj1 == obj2); //=> true
console.log(obj1 === obj2); //=> true

const obj3 = obj1;
obj3.b = 10;

console.log(obj1.b); //=> 10
console.log(obj3.b); //=> 10
console.log(obj1 == obj3); //=> true
console.log(obj1 === obj3); //=> true

// Immutable.Map の場合
const Immutable = require("immutable");

const map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set("b", 2); //=> #1

console.log(map1.get("b")); //=> 2
console.log(map2.get("b")); //=> 2
console.log(map1 == map2); //=> true
console.log(map1 === map2); //=> true
//変更するメソッドsetを使用しても、値に変更がない場合は同一

const map3 = map1.set("b", 10);
console.log(map1.get("b")); //=> 2
console.log(map3.get("b")); //=> 10
console.log(map1 == map3); //=> false
console.log(map1 === map3); //=> false
//値が変更された場合は、同一なオブジェクトではない

前述の通りImmutable.jsでは、変更を加えるメソッドは新しいオブジェクトを返す。
しかし、実際に変更がされていない場合には、新しいオブジェクトにならない。
この点はよくできてるなーというのが所感。

そのほかの immutable.js の提供するオブジェクト

Stack

1
2
3
4
5
6
7
8
9
10
11
12
const Immutable = require("immutable");

// Stack いわゆるFILOのデータ構造
const stack = Immutable.Stack([1, 2, 3]);
const stack1 = stack.push(4);
const stack2 = stack1.push(5);
const stack3 = stack2.shift();

console.log(stack.toJS()); //=> [ 1, 2, 3 ]
console.log(stack1.toJS()); //=> [ 4, 1, 2, 3 ]
console.log(stack2.toJS()); //=> [ 5, 4, 1, 2, 3 ]
console.log(stack3.toJS()); //=> [ 4, 1, 2, 3 ]

Seq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Seq 遅延評価によりメモリ効率よく処理しやすくなる? (遅延評価は後述)
// List と同じように操作できる
const seq = Immutable.Seq([1, 2, 3]);

console.log(seq.map((x) => x * x).toJS()); // => [1,4,9]
console.log(seq.reduce((x, y) => x + y * y)); // => 14
console.log(seq.filter((x) => x <= 2).toJS()); // => [1,2]
console.log(
seq
.filter((x) => x <= 2)
.map((x) => x * x)
.toJS()
); // => [1,4]

const threerdPower = (collection) => {
return collection.map((x) => x ** 3);
};

console.log(
seq
.filter((x) => x <= 2)
.update(threerdPower)
.toJS()
); // => [1,8]

Range

1
2
3
4
5
6
// Range
// 指定した範囲を指定した感覚でカウントアップ(もしくはカウントダウン)する
console.log(Immutable.Range(10, 20).toJS()); // => [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
console.log(Immutable.Range(10, 20, 2).toJS()); // => [ 10, 12, 14, 16, 18 ]
console.log(Immutable.Range(20, 10, 2).toJS()); // => [ 20, 18, 16, 14, 12 ]
console.log(Immutable.Range().toJS()); // => 0からInfinityまで範囲の結果エラーになる。ドキュメントにはあるけど使い方がよくわからない。

Repeat

1
2
3
4
// Repeat
// 指定した引数を指定回数繰り返す
console.log(Immutable.Repeat("XXXX", 5).toJS()); // => [ 'XXXX', 'XXXX', 'XXXX', 'XXXX', 'XXXX' ]
console.log(Immutable.Repeat("XXXX").toJS()); // => 無限に同じ文字列を出力し続ける結果エラーになる。ドキュメントにはあるけど使い方がよくわからない。

Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Set
// 重複のないように管理してくれるコレクション
const setObj = Immutable.Set([1, 2, 3, 4]);
const setObj1 = setObj.add(1);
const setObj2 = setObj.add(5);

console.log(setObj.toJS()); // => [ 1, 2, 3, 4 ]
console.log(setObj1.toJS()); // => [ 1, 2, 3, 4 ]
console.log(setObj2.toJS()); // => [ 1, 2, 3, 4, 5 ]

const setObjA = Immutable.Set([1, 2, 3, 4]);
const setObjB = Immutable.Set([3, 5, 6, 4]);
const setObjUnion = Immutable.Set.union([setObjA, setObjB]);
const setObjIntersect = Immutable.Set.intersect([setObjA, setObjB]);

console.log(setObjUnion.toJS()); // => [ 3, 5, 6, 4, 1, 2 ]
console.log(setObjIntersect.toJS()); // => [ 3, 4 ]

// OrderedSet
// 確認した範囲だとSetと違いがわからなかったので、スキップ

Record

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Record
// 設定したキーだけを許可し、デフォルト値を持つオブジェクトみたいなもの
const myRecord = Immutable.Record({ a: 1, b: 2, c: "CC", d: "DD" });
const myrecord = myRecord();
const myrecord1 = myRecord({ c: "CCCC" });
const myrecord2 = myRecord({ a: 100 });

console.log(myrecord.toJS()); // => { a: 1, b: 2, c: 'CC', d: 'DD' }
console.log(myrecord1.toJS()); // => { a: 1, b: 2, c: 'CCCC', d: 'DD' }
console.log(myrecord2.toJS()); // => { a: 100, b: 2, c: 'CC', d: 'DD' }

const myrecord3 = myRecord({ e: 100 }); // => 設定していないキーに値を設定する

console.log(myrecord2.get("a")); // => 100
console.log(myrecord3.get("e")); // => undefined Recordで指定していないキーはundefinedになる

const myrecord2_1 = myrecord2.set("a", 10000);
const myrecord2_2 = myrecord2.set("f", 99999);

console.log(myrecord2_1.get("a")); // => 10000 値の更新もできる
console.log(myrecord2_2.get("f")); // => undefined Recordで指定していないキーは更新でもundefinedになる

const myrecord2_3 = myrecord2_1.remove("a");

console.log(myrecord2_3.get("a")); // => 1 値を削除すると設定したデフォルト値に戻る

Record を使用して、モデルの実装をしたりという記事が良く見つかります。
これは、また追々試すでしょう。

遅延評価って何だろう

「遅延評価」という用語があるが、ちゃんと理解していなかったので、少し調べました。
Wikipedia の遅延評価の項目より抜粋です。

評価しなければならない値が存在するとき、実際の計算を値が必要になるまで行わないことをいう。

遅延評価を行う利点は計算量の最適化である。

よくわからないので、具体的な説明を探しました。

Qiita - JS のコレクション操作ライブラリーに対する雑な所感
こちらにあるコードを参考にしつつ、以下を試しました。

遅延評価しないパターン
1
2
3
4
5
6
7
8
9
10
const list = Immutable.List([1, 2, 3, 4]);

const result_l = list
.map((x) => {
console.log(x);
return x * x;
})
.take(2);
console.log("[result]");
console.log(result_l.toJS());
遅延評価しないパターン 実行結果
1
2
3
4
5
6
1
2
3
4
[result]
[ 1, 4 ]

遅延評価しないパターンは、「いわゆるこうなるかな?」の想定通りの実行・表示をしていると感じます。
続いて遅延評価するパターンです。

遅延評価するパターン
1
2
3
4
5
6
7
8
9
10
const seq = Immutable.Seq([1, 2, 3, 4]);

const result_s = seq
.map((x) => {
console.log(x);
return x * x;
})
.take(2);
console.log("[result]");
console.log(result_s.toJS());
遅延評価するパターン 実行結果
1
2
3
4
[result]
1
2
[ 1, 4 ]

最終的に先頭から 2 つの値しか使用しないことが判断されて、3 と 4 を 2 上する処理がスキップされています。
確かに
最終的な値の表示に不要な処理を削減し計算量の最適化ができていました。


今回は、 immutable.js を触ってみました。
イミュータブルに値を扱う利点を感じられました。
よくわかっていなかった遅延評価も、比較してみると処理の流れが異なっているので、具体的な確認ができてよかったと感じました。

ではでは。