代打で雑な記事書いときます。
今回はJSのコレクション操作ライブラリーに対する雑な感想を述べときます。
コレクションという言葉は配列とかオブジェクトの総称としてここで勝手に使ってる言葉です。適切な言葉があれば教えてください。
いきなり雑な所感
コレクション操作をするライブラリーの有名なところとして
が挙げられると思います。
あまりちゃんと使ったわけではないのですが、賢い遅延評価をするLodash likeなライブラリーとしてLazy.jsなんてものもあります。
最近ではReduxとかを使う上でImmutabilityが求められるため
以下に挙げるようなライブラリーが使われるようになってきているのかなという印象です。
それ以外にもオブジェクトのimmutableな操作に特化したicepickなんてものもあります。
またImmutable.jsみたいにコレクション操作後にいちいちJSの配列やオブジェクトに戻すのが面倒くさいという人のためにseamless-immutableなんてものも出ています。
さてここまで挙げたライブラリーに対しての私の勝手な所感をまとめておきます。
ライブラリー | 所感 |
---|---|
Underscore.js | APIが少なすぎる。未だにES3を書かざるを得ない人は使う機会がある? |
Lodash | APIが物足りない。操作がmutableなので困るときがある |
Lazy.js | 遅延評価が賢いが、Lodash likeなためやはりAPIが物足りない |
icepick | APIが物足りない |
seamless-immutable | 確かにそのまま配列やオブジェクトとして使えるのはありがたいが、APIが物足りない |
Immutable.js | APIは十分。しかもOrderedMapやOrderedSetみたいな便利な構造体も用意されているためこれらを使いたいときにはこれ一択 |
Ramda | APIは豊富。Lispとかに慣れている人は使いやすいのかもしれない。慣れてない自分は関数名でどんな操作ができるかわからなかったので結構困惑した |
ちょっと踏み込んだ所感
上記所感でわかると思いますが、私が今使うライブラリーはImmutable.jsかRamdaのどちらかです。
もう少し細かく両者に対する感想を述べておこうと思います。
- Immutable.js
- pros
- API名でだいたいどんな操作ができるのか想像つく。もともとJSにある関数とは引数の順序などに関して互換性を保っているのも人によってはとっつきやすいポイントだと思う
- API数は豊富で今のところ何か足りないと感じたことはない
- 便利な構造体を用意してくれてる(OrderedMap, OrderedSet, etc.)
- Jestと相性が良く、いちいち配列やオブジェクトに戻さなくてもassertできたりする (ただの囲い込み戦略といえばそれまでだが)
- redux-immutableとかを使えばRedux使う上での煩わしさも軽減
- cons
- とはいえいちいち配列やオブジェクトに戻す操作が面倒に感じるときは多い。この辺はstaticメソッド的なものを用意してくれてると解決すると思う
- APIドキュメントがstableバージョンのものではなく、rcのものも含め最新バージョンで公開されているので、stableにまだ存在しない関数を使って怒られることもしばしば
- トップページでいきなりAlan Kayの写真で威圧してくる
- pros
- Ramda
- pros
- APIはそれなりに豊富で、たまに足りないと思うこともあるが概ね満足
- Ramda独自の構造体などは用いないため、変換の手間がなくてお手軽 (お手軽と言えるようになるまでにはもちろん苦労しています)
- カリー化を利用した関数合成でメソッドチェーンライクな操作が可能でかっこいい
- カリー化のわかりやすい記事はこちら - 関数合成で新たな関数を作るときに名前が付けやすい。中間変数に変に冗長な名前を付けるよりも、操作に対して名前を付ける方が圧倒的に楽で自然だということが実感できる
- cons
- pros
例えばオブジェクトをマージしたい場合の例を以下に示しておく。(Node.js 8.9.3で実行してます)
const I = require('immutable');
const R = require('ramda');
const object = {a: 1, b: 2};
const result1 = I.Map(object).merge({c: 3}).toJSON();
console.log(result1);
const result2 = R.merge(object, {c: 3});
console.log(result2);
console.log("Wow! Immutable!");
console.log(object);
$ node merge
{ a: 1, b: 2, c: 3 }
{ a: 1, b: 2, c: 3 }
Wow! Immutable!
{ a: 1, b: 2 }
今のところは遅延処理の必要性を感じる機会も少ないので、お手軽さ的にRamdaを使うことが多いです。
ただImmutable.js 4.0.0からはお手軽なmerge()なども登場するようで、そうするとだんだんImmutable.js一択になりそうな気がしてます。
(ちなみに私も使いやすいライブラリーでも作ろうかと思っているのですが、弊社の優秀な後輩に「ていうかelmとかPureScript使えばいいっすよね」的な心無い一撃をお見舞いされて心折れかけたまま放置気味です)
賢い遅延評価
賢い遅延評価について雑に解説しておきます。
まずは簡単に理解するためのコードを示しておきます。
const I = require('immutable');
const L = require('lazy.js');
const R = require('ramda');
const _ = require('lodash');
const array = [1, 2, 3, 4, 5];
const result1 = I.Seq(array)
.map((i) => {
console.log(`Immutable.js: ${i}`);
return i + 1;
})
.reverse()
.take(3);
console.log('Immutable.js!!!');
console.log(result1.toJSON());
const result2 = L(array)
.map((i) => {
console.log(`Lazy.js: ${i}`);
return i + 1;
})
.reverse()
.take(3);
console.log('Lazy.js!!!');
console.log(result2.value());
const mapShiteReverseKaranoTake = R.pipe(
R.map((i) => {
console.log(`Ramda: ${i}`);
return i + 1;
}),
R.reverse(),
R.take(3),
);
console.log('Ramda!!!');
console.log(mapShiteReverseKaranoTake(array));
const result4 = _.chain(array)
.map((i) => {
console.log(`Lodash: ${i}`);
return i + 1;
})
.reverse()
.take(3);
console.log('Lodash!!!');
console.log(result4.value());
$ node index
Immutable.js!!!
Immutable.js: 5
Immutable.js: 4
Immutable.js: 3
[ 6, 5, 4 ]
Lazy.js!!!
Lazy.js: 5
Lazy.js: 4
Lazy.js: 3
[ 6, 5, 4 ]
Ramda!!!
Ramda: 1
Ramda: 2
Ramda: 3
Ramda: 4
Ramda: 5
[ 6, 5, 4 ]
Lodash!!!
Lodash: 5
Lodash: 4
Lodash: 3
[ 6, 5, 4 ]
(あれ?遅延評価してない例としてLodashで試してみたら賢く遅延評価してるじゃないか)
賢い遅延評価というのを確認するために今回はmap()
の中でconsole.log()
を実行している。実際に操作が行われたのがいつかを確認するためです。
結果の通り、Ramda以外は2
と1
を出力していないことがわかる。
これはtake(3)
でどうせ使われなくなる部分のi + 1
の評価はしないくて良いと判断して実行していないということである。
しかもいやらしくreverse()
を挟んでいるのにちゃんとどこが必要ない計算なのか判断している。賢い感じしません?(内部的にはIteratorを使って賢く実装されています。ちゃんとした解説はまだできないです。すいません)
実際計算量が減るので必要とする人もいるはず。
take()
だけでなくsome()
(any()
)やevery()
(all()
)みたいな関数を使っても確認可能です。
Ramdaの場合はあくまで関数を合成しているだけで、map()
=> reverse()
=> take()
を素直に実行する関数を作っているだけのようです。
あとconsole.log('Immutable.js!!!');
の前にそもそもmap()
やreverse()
、take()
を実行していない点も実は遅延評価と呼べます。
Immutable.jsのListを使ってみるとその違いがわかりやすいです。
const I = require('immutable');
const array = [1, 2, 3, 4, 5];
const result = I.List(array) // I.fromJS(array)でも可
.map((i) => {
console.log(i);
return i + 1;
})
.reverse()
.take(3);
console.log('Immutable.js!!!');
console.log(result.toJSON());
$ node list
1
2
3
4
5
Immutable.js!!!
[ 6, 5, 4 ]
おわりに
雑でまとまりのない記事になって申し訳ないですが、今回はいろんなコレクション操作ライブラリーの所感と特徴を紹介しました。
少しでも皆様が使うときに参考になると嬉しいです。
そして自分のライブラリーのキャッチアップがあまいということを今回実感しましたので、ここ違うよとか知識が古いよみたいな意見がありましたら
指摘していただけるとありがたいです。(Lodashなめてました。すいません)