Immutable.js の使い方 (基本メソッド編)

現在 Qiita にある [Immutable.js](http://facebook.github.io/immutable-js/docs/) の記事は Redux の文脈におけるものが多い (気がする). 単純な Immutable.js の解説となるものが少なかったため, あくまで「素の」 Immutable.js の使い方といった観点で各種データ型とその用途をまとめてみます.

なお本稿は個人的な勉強のための備忘録のため, 内容の正確性は担保できません. おかしいなと思ったら公式のドキュメントに拠りましょう. また不備や誤りを見付けた際はコメントなどで指摘していただけると助かります.

また内容が長くなったため, まずは Immutable.js の概要および基本的なメソッドの説明にとどめ, 前半とします.

# Immutable.js とは

Facebook 製のイミュータブルなデータ操作を可能にする js ライブラリ. 同様のライブラリとしては

- [Ramda.js](https://ramdajs.com/) (参考記事 : [Ramda.jsを使った関数型プログラミングの実践 - WPJ](https://www.webprofessional.jp/functional-programming-with-ramda/))
- [Lodash](https://lodash.com/)

などがあるが, これらのライブラリが通常の js オブジェクトや配列に対するイミュータブルな操作方法を提供するのに対し, Immutable.js は独自のデータ型 (クラス) を定義しているといった違いがある. このあたりの選定はお好みで.

個人的には Immutable.js が提供する各種データ型を使い分けることでコードの見通しがよくなるように感じているため, 以下ではそのあたりも含めてまとめていきたいと思う.

## 導入方法

```bash
npm install immutable
```

して, 

```js
import * as I from 'imuutable';
```

で, `I.List` や `I.Map` といった形で各種データ型を呼び出せるようになる. import 文の書き方はお好みで.

なお上記導入方法は Node + Webpack 環境を想定しているので, 詳細や他の方法は公式のドキュメントに拠ってください.

### Chrome Devtools 拡張

Immutable.js は独自のデータ型を js で実現しているため, そのまま `console.log()` しても訳の分からないオブジェクトが出力される (いちおう `collection.toJS()` メソッドなどで通常の js オブジェクトに変換することは可能). なのでデバッグ用として以下の Chrome 拡張を導入しておくことをオススメします.

- [Immutable.js Object Formatter](https://chrome.google.com/webstore/detail/immutablejs-object-format/hgldghadipiblonfkkicmgcbbijnpeog?hl=en)

これによって List や Map 型の内容が見やすくなるだけでなく, 後述する `I.Record` で定義した独自のクラスについても名前が表示されるため, 非常にデバッグがやりやすくなる.

# Immutable.js の使い方

## 基本操作 - Collection 型

Immutable.js が提供する様々なデータ型は全て Collection というクラスを継承しています. そのため, (Collection 型を直接使用する機会はおそらくないものの) このクラスに生えているメソッドを押さえておくことで Immutable.js の基本的な使い方が分かるかと思います.

参考 : [Collection — Immutable.js](http://facebook.github.io/immutable-js/docs/#/Collection)

### データ型作成

```js
collection = I.Collection();
```

基本的に Immutable.js が提供する各種トップレベルの関数は Factory なので, 上記のような感じでデータ型を生成する.

Collection型自体は iteratable な Key - Value 型のデータ構造とのこと.

### データ読み取り系メソッド

```js
collection.get('key') // 指定の Key に対応する Value を返す
collection.getIn(['key1','key2','key3']) // 入れ子になっているデータ構造において, 親から順に Key をたどっていき Value を返す

collection.has('key') // 指定の Key が存在するかの boolean を返す
collection.hasIn(['key1','key2','key3']) // getIn と同様に has() の deep 版

collection.includes('value') // 指定の Value が存在するかどうかの boolean を返す

collection.first() // 先頭の要素の Value を返す
collection.last() // 末尾の要素の Value を返す
```

ここで便利なのは `getIn` や `hasIn` などの deep 系のメソッド.

先述したように Immutable.js の全てのデータ型はこの Collection 型を継承しているため, List, Map, Record など **複数種類のデータ型を入れ子にしていても, 気にせず `getIn` や `hasIn` などで深い層にアクセスできる** ようになる.

また path を文字列の配列で指定するため, (記述が少々冗長になりがちだが) 同じ構造を持つ複数データ型同士での取り回しがよくなり, また path の途中で参照が切れたとしても (指定する Key が存在しなくても) 最終的に **`undefined` を返すのみでエラーにならない** ため使い勝手が良い.

### Sequence 系メソッド

```js
collection.map(val => return val * 2) // 各要素の Value を2倍した **新しい Collection を返す**

collection.filter(val => return val > 1) // Value が1より大きい要素のみになるようフィルタリングした新しい Collection を返す. ※ predicate 関数は boolean を戻せばよい
collection.filterNot(val => return val > 1) // Value が1より大きくない (以下) の要素のみ (略)

collection.reverse() // 順序をひっくり返した新しい Collection を返す

collection.sort(comparator(valA,valB)) // 2つの Value を引数にとる任意の関数でソートし, 新しい Collection を返す
// ソート用関数 (comparator) が負の数を戻すときはA→Bの順, 正の数のときはB→Aの順で要素を入れ替え, 0のときは順序を変えない
// comparator を省略した場合, Value による単純な昇順ソートになる
collection.sortBy(mapper(val), comparator(mappedValA,mappedValB)) // より詳細な条件でのソートのため, Value を comparator に渡す前段階で mapper 関数を通す. ここでも comparator 自体は省略可

collection.groupBy(mapper(val)) // mapper関数の戻す値によって要素をグループ化する
// 返される Collection は mappedVal を Key にとる Map 型で, それぞれの Value は該当の元要素を格納した List 型になるっぽい
```

このあたりのメソッドはだいぶ Lodash や underscore っぽく, 同ライブラリを触ったことがある方にはとっつきやすいと思います. 何より継承元の Collection でこれの操作を押さえられてあることで, Immutable.js 全体を通して一貫した感覚でデータを触れるのがいい.

ちなみに本稿ではイミュータブル性まわりの詳しい説明は省くが, 上記含め全てのメソッドは常に新しい Collection を返し, 元の collection 自体は変更されないようになっている.

またこれらのメソッドの返り値は呼び出し元と同じデータ型になる.

### 副作用系メソッド

```js
collection.forEach(sideEffect(val,key,iter))
```

各要素の Value, Key , および iter として元の Collection 自体を取って sideEffect 関数を実行する. これらの引数はもちろん後ろから順に省略可能.

pure js の `Array.forEach` と違い, sideEffect の戻り値が false となった時点で処理を停止し, forEach メソッド自体は処理が行われた要素 (最後に false を返した要素を含む) の個数を返す (元の Collection が返るわけではないため, メソッドチェーンの途中に組み込めないことに注意).

### Subset 作成系メソッド

```js
collection.slice(begin, end) // 指定した範囲 (startは含むがendは含まない) で要素を切り出したサブセットを返す.
// それぞれ0始まりの数値で, 負の数の場合は後ろから数える (-2は後ろから2番目を指す)
// endを省略した場合は最後までを切り出す

collection.rest() // 最初の要素のみ省いた Collection を返す, たぶん .slice(1) と同じ
collection.butLast() // 最後の要素のみ省いた Collection を返す, たぶん .slice(0,-1) と同じ

collection.skip(2) // 最初の2要素のみ省いた Collection を返す, たぶん .slice(2) と同じ
collection.skipLast(2) // 最後の2要素のみ省いた (略)
collection.skipWhile(predicate(val,key)) // predicate が先頭要素から連続して「true になっている間は」スキップし, 残りの Collection を返す
// 正確には predicate が「はじめて false になる」要素が先頭になるように切り出す
collection.skipUntil(predicate(val,key)) // predicate が「はじめて true になる要素まで」をスキップし, 残りの Collection を返す

collection.take(2) // 最初の2要素を取り出す
collection.takeLast()
collection.takeWhile(predicate(val,key))
collection.takeUntil(predicate(val,key))
```

これらのメソッドの返り値についても, 呼び出し元と同じデータ型になる.

### Combination 系メソッド

```js
collection.concat(...valuesOrCollections: Array)
collection.flatten(depth?: number)
collection.flatten(shallow?: boolean)
collection.flatMap(mapper: (value: V, key: K, iter: this) => Iterable<M>)
```

これらのメソッドについてはドキュメントにサンプルコードがなく, 手元でも試せていないためいったん省略. 理解できている方はコメントなどで教えていただけると助かります.

### Reduce 系メソッド

```js

collection.reduce(reducer(reduction, key,val,iter), initialReduction)
// initialReduction を省略した場合, 先頭要素で代替され, 処理は2番目の要素から始まる (基本的には省略することの方が多い)
collection.reduceRight() // .reduce() の逆順処理, .reverce.reduce() とするのと同じ
```

`Array.reduce` と同様の動作. よく分からない方のために説明すると, 

1. はじめ reducer 関数は「先頭要素 (の値)」と「2番目の要素 (の値)」を引数にとり, 
2. 何らかの処理の結果, **1-2番目の要素 (の値) のまとめ** として reduction を戻す
3. 次に reducer 関数は「1-2番目の要素のまとめ (reduction)」と「3番目の要素 (の値)」を引数にとり, 
4. 同様の処理を行い, **1-3番目の要素のまとめ** として新たな reduction を戻す
5. 繰り返し
6. 最終的に **リストの全要素 (の値) のまとめとしてひとつの reduction が返される**

リストを先頭から2要素ずつガシガシまとめていき, 最終的にひとつの値にまで落とし込むイメージ. 使用感の詳細については [MDN などで Array.reduce() を調べれば](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 分かるはず.

また reduce のサブセット的なメソッドとして,

```js
collection.every(predicate(key,val,iter)) // 全要素において predicate が true ならば true を返す
collection.some(predicate(key,val,iter)) // 1要素でも predicate が true ならば true を返す

collection.join(', ') // 各要素の Value を文字列として ', ' で繋いで返す

collection.isEmpty() // 一切の Value を含まない場合 true を返す

collection.count() // 要素数を返す
collection.count(predicate(key,val,iter)) // predicate を満たす (true を戻す) 要素数を返す

collection.countBy(grouper(key,val,iter)) // .groupBy に近いが, 返される Map の Value が要素数を示す数値になる
```

### find 系メソッド

```js
collection.find(predicate(key,val,iter)) // 先頭から predicate を見ていき, はじめに true となる要素の Value を返す
collection.findLast(predicate(key,val,iter)) // .find の逆順 (末尾から探す)

collection.findEntry(predicate(key,val,iter)) // 返り値が [Key, Value] の配列になる
collection.findLastEntry(predicate(key,val,iter))

collection.findKey(predicate(key,val,iter)) // 返り値が該当要素の Key になる
collection.findLastKey(predicate(key,val,iter))

collection.keyOf(val) // Value が val になる最初の要素の Key を返す
collection.kstKeyOf(val)

collection.max(comparator(valA,valB)) // .sort と同じ使い方, Value が返され, comparator を省略した場合は > が代わりに使われる
collection.maxBy(mapper(val), comparator(valA,valB)) // .sortBy と同じ使い方 (略)
collection.min(comparator(valA,valB))
collection.minBy(mapper(val), comparator(valA,valB))
```

### 比較系メソッド

```js
collection.equals(any) // any と同値かどうか

collection.isSubset(iter) // 全ての Value が iter に含まれているかどうか
collection.isSuperset(iter) // iter の持つ全て Value を含んでいるかどうか
```

# まとめ

本稿では Immutable.js の使い方の前半として Collection データ型を通じて基本的なメソッドについてまとめてみました. 様々あるデータ型の紹介が未だのため正直ありがたみが分かりづらいが, まずはこれだけあるメソッドが Immutable.js 全体を通じて共通して使える, といったていどに理解してもらえればと思います.

ちなみにデータ型変換系のメソッドについては省略したため, ここにあるメソッドが全てではない. 公式のドキュメントは Typescript 形式で書かれており結構分かりやすいため, 必ず一度は目を通しておいて欲しい.

残り後半についても近日中にまとめる予定.