最近、ググっていると 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); console.log(arr_b);
|
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); console.log(arr_d);
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); console.log(obj_f);
|
...
を先頭につけると、配列とオブジェクトを展開できる。
展開するとき、変更・追加したいものを付与すると別のオブジェクトになるので、元のオブジェクトに影響が無い。
変更追加はできたのだが、特定の項目削除はできないようだった。
スプレッド構文でイミュータブルなオブジェクトの扱いをするためにできるのは、Deep Copy
+Deep 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.js1 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
| const arr1 = [1, 2, 3]; const arr2 = arr1; arr2.push(4);
console.log(arr1); console.log(arr2); console.log(arr1 == arr2); console.log(arr1 === arr2);
const Immutable = require("immutable"); const list1 = Immutable.List([1, 2, 3]); const list2 = list1;
console.log(list1.toJS()); console.log(list2.toJS()); console.log(list1 == list2); console.log(list1 === list2);
const list3 = list1.push(4); list3.push(5); list3.push(6); list3.push(7);
console.log(list1.toJS()); console.log(list3.toJS()); console.log(list1 == list3); console.log(list1 === list3);
|
Immutable.js
では、変更を加えるメソッドは新しいオブジェクトを返す。
なので、Array オブジェクトの末尾に追加する push などと同じメソッド名前を呼び出しても動作が異なる。
変更を加えた=違うオブジェクトなので、比較が容易。
オブジェクト
Immutable.js のImmutable.Map
は、JavaScript の Object
に該当するでしょう。
src.js1 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
| const obj1 = { a: 1, b: 2, c: 3 }; const obj2 = obj1; obj2.b = 2;
console.log(obj1.b); console.log(obj2.b); console.log(obj1 == obj2); console.log(obj1 === obj2);
const obj3 = obj1; obj3.b = 10;
console.log(obj1.b); console.log(obj3.b); console.log(obj1 == obj3); console.log(obj1 === obj3);
const Immutable = require("immutable");
const map1 = Immutable.Map({ a: 1, b: 2, c: 3 }); const map2 = map1.set("b", 2);
console.log(map1.get("b")); console.log(map2.get("b")); console.log(map1 == map2); console.log(map1 === map2);
const map3 = map1.set("b", 10); console.log(map1.get("b")); console.log(map3.get("b")); console.log(map1 == map3); console.log(map1 === map3);
|
前述の通りImmutable.js
では、変更を加えるメソッドは新しいオブジェクトを返す。
しかし、実際に変更がされていない場合には、新しいオブジェクトにならない。
この点はよくできてるなーというのが所感。
そのほかの immutable.js の提供するオブジェクト
Stack
1 2 3 4 5 6 7 8 9 10 11 12
| const Immutable = require("immutable");
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()); console.log(stack1.toJS()); console.log(stack2.toJS()); console.log(stack3.toJS());
|
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
|
const seq = Immutable.Seq([1, 2, 3]);
console.log(seq.map((x) => x * x).toJS()); console.log(seq.reduce((x, y) => x + y * y)); console.log(seq.filter((x) => x <= 2).toJS()); console.log( seq .filter((x) => x <= 2) .map((x) => x * x) .toJS() );
const threerdPower = (collection) => { return collection.map((x) => x ** 3); };
console.log( seq .filter((x) => x <= 2) .update(threerdPower) .toJS() );
|
Range
1 2 3 4 5 6
|
console.log(Immutable.Range(10, 20).toJS()); console.log(Immutable.Range(10, 20, 2).toJS()); console.log(Immutable.Range(20, 10, 2).toJS()); console.log(Immutable.Range().toJS());
|
Repeat
1 2 3 4
|
console.log(Immutable.Repeat("XXXX", 5).toJS()); 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
|
const setObj = Immutable.Set([1, 2, 3, 4]); const setObj1 = setObj.add(1); const setObj2 = setObj.add(5);
console.log(setObj.toJS()); console.log(setObj1.toJS()); console.log(setObj2.toJS());
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()); console.log(setObjIntersect.toJS());
|
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
|
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()); console.log(myrecord1.toJS()); console.log(myrecord2.toJS());
const myrecord3 = myRecord({ e: 100 });
console.log(myrecord2.get("a")); console.log(myrecord3.get("e"));
const myrecord2_1 = myrecord2.set("a", 10000); const myrecord2_2 = myrecord2.set("f", 99999);
console.log(myrecord2_1.get("a")); console.log(myrecord2_2.get("f"));
const myrecord2_3 = myrecord2_1.remove("a");
console.log(myrecord2_3.get("a"));
|
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());
|
最終的に先頭から 2 つの値しか使用しないことが判断されて、3 と 4 を 2 上する処理がスキップされています。
確かに
最終的な値の表示に不要な処理を削減し計算量の最適化ができていました。
今回は、 immutable.js を触ってみました。
イミュータブルに値を扱う利点を感じられました。
よくわかっていなかった遅延評価も、比較してみると処理の流れが異なっているので、具体的な確認ができてよかったと感じました。
ではでは。