Lazy.js を紹介してみる
以前、 underscore.js というものを紹介したことがあったのだけど、今回は Lazy.js という underscore.js like なライブラリを紹介したいと思う。
ホームページはこちらから。
Lazy.js - v0.3.2
インストールはコマンドプロンプトで
npm install lazy.js --save
That's all!! so really easy.
何故、 Lazy.js を使うのか
イントロダクションを読めば分かるけど、 underscore.js で大きなコレクションを扱おうとするとパフォーマンスが気になってきます。そうなると、愚直な手続き的なコードを書かざるを得なくなる。
イントロダクションから抜粋すると例えばこういう例
//これは Lazy.js を使って Array を作っているけど些細な問題なので気にしないでください。 var array = Lazy.range(1000).toArray(); // -> [0, 1, 2, ... 997, 998, 999] function square(x) { return x * x; } function inc(x) { return x + 1; } function isEven(x) { return x % 2 === 0; } var result = _.chain(array).map(square).map(inc).filter(isEven).take(5).value();
この場合 underscore.js だと遅いんですね。
だから、恐らくパフォーマンスを気にする場合、以下のように書くでしょう。
var results = []; for (var i = 0; i < array.length; ++i) { var value = (array[i] * array[i]) + 1; if (value % 2 === 0) { results.push(value); if (results.length === 5) { break; } } }
首尾よくまとまったように見えます。何が問題なのでしょう。
そう、このブログを読んでいるようなプログラマの皆様はお分かりと思いますが、一度しか使えないコードで汎用性がないんですよね。
それを解決するのが Lazy.js というわけです。
var result = Lazy(array).map(square).map(inc).filter(isEven).take(5);
そして Lazy.js はほとんど underscore.js と同じ名前で同じような振る舞いをする関数群を提供します。ただし、返り値としては sequence オブジェクトが返ってくるのでそのへんが一番の大きな違いになります。
大事なこととして each を呼ぶまで Lazy.js は繰り返し処理をしないし、配列を何度も作るようなことはしないということかな。 Lazy.js は全てのクエリをシーケンスの中で結合して振る舞いを手続き的なコードと同じようにしている*1。
だから、パフォーマンスが気になるけど手続き的なコードを書くのが嫌いな人は Lazy.js を使うといいよ!というようなことがイントロダクション中に書いてあります。
大きな面白いトピックが幾つかありますが、とりあえずスキップします(Indefinite sequence generation とか Asynchronous iteration めちゃくちゃ面白い)。
さて、軽く Lazy.js を紹介したところで実際に面白そうな/実用的な関数*2を紹介していきましょう。
Lazy
var Lazy = require('lazy.js'); var likeArray = Lazy([1, 2, 3]), likeObject = Lazy({foo: 1, bar: 2}), likeString = Lazy('Hello, world'), asyncSeq = likeArray.async(); console.log(likeArray); // -> ArrayWrapper{ /* something */ } console.log(likeObject); // -> ObjectWrapper{ /* something */ } console.log(likeString); // -> StringWrapper{ /* something */ } console.log(asyncSeq); // -> AsyncSequence{ /* something */ }
だいたいこんな感じで Wrapper のオブジェクトを返してくれますが、全て sequence をプロトタイプに持つオブジェクトです。
undefined や null を渡すと空の sequence を返すという安全仕様ですね
crateWrapper 関数
var factory = Lazy.createWrapper(function(eventSource) { var sequence = this; eventSource.handleEvent(function(data) { sequence.emit(data); }); }); var eventEmitter = { triggerEvent: function(data) { eventEmitter.eventHandler(data); }, handleEvent: function(handler) { eventEmitter.eventHandler = handler; }, eventHandler: function() {} }; var events = []; factory(eventEmitter).each(function(e) { events.push(e); }); eventEmitter.triggerEvent('foo'); eventEmitter.triggerEvent('bar'); events // => ['foo', 'bar']
この関数、最初挙動が意味分からなかったんだけど、 StreamLikeSequence を返す factory を作れて、その中で emit を使うような関数を定義しておくとその関数が呼び出された時に sequence に伝搬させることができるのですね。 StreamLikeSequence は AsyncSequence をプロトタイプに持つので非同期なのですねー。ちょっと難しいけど、実用的かなと。
generate 関数
var counter = Lazy.generate(function(i){ return i*i; }); console.log(counter.take(10).toArray()); // -> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
文字通り generator を作れます。フィボナッチ数列を返すような generator も作れるので便利です。
repeat 関数
var hello = Lazy.repeat('Hello, world'); console.log(hello.take(10).toArray()); // -> ["Hello, world", ..., "Hello, world"] var hellof = Lazy.repeat(function(){console.log('Hello');}); hellof.take(10).each(function(f){f();}); // -> 10 times 'Hello'
like generate 関数という感じ関数。ただ、単一のものしか扱えないのでそのへんが違うとこ。便利ー。
Sequence
Sequence はストリームやコレクションといったゼロ以上の要素をもつ概念を統一的に扱える API を提供しています。だいたい、このオブジェクトは他の後述するオブジェクトのプロトタイプになっているはずなので、この API は基本的なものだと思ってもらえれば OK です。たぶん。
async 関数
var fn = function(n){console.log(n);}; Lazy('Good night').async(3).each(fn); Lazy('Good morning').reverse().async(1).each(fn);
シーケンスを非同期オブジェクト*3にして実行出来るので、大きなシーケンスを処理したいときなどには後発の処理を止めずに済むのでいいですね。
consecutive 関数
Lazy.range(10).consecutive(3); // -> [[0,1,2], [1,2,3], ..., [7,8,9]]
用途不明だけど順序で何個ずつ取るみたいな処理で使えそう。けど、一生僕は縁がなさそう。
countBy 関数
var resultOfExams = [ {name: 'ayato_p', score: '60'}, {name: 'zer0_u', score: '70'}, {name: 'alea12', score: '85'}, {name: 'naoiwata', score: '99'}, {name: 'iriya_ufo', score: '75'}, {name: 'shohe_i', score: '90'} ]; var judge = function(result){ return result.score >= 80 ? 'awesome' : 'bad'; }; console.log(Lazy(resultOfExams).countBy(judge).toObject()); // -> {bad: 3, awesome: 3}
これは良いですね。これは新しい sequence を作るので非破壊的ですし、良いと思います。ちなみに countBy は文字列を引数に取ることもできるのですね。
dropWhile 関数
var score = function(result){ return result.score; }; Lazy(resultOfExams).sortBy(score).reverse().dropWhile(judge).each(function(result){ console.log(result.name); // -> iriya_ufo, zer0_u, ayato_p });
条件にマッチしなくなるまで drop していくという関数です。
findWhere 関数
console.log(Lazy(resultOfExams).findWhere({name: 'naoiwata'})); // -> {name: "naoiwata", score: "99"}
オブジェクトのプロパティで find する関数ですねぇ。
ofType 関数
console.log(Lazy(['foo', 1, 2, new Date(), undefined]).ofType('number').toArray()); // -> [1, 2]
指定した型のみの sequence を新しく作りますが、この関数は typeof 演算子の評価する値でしか取れないので例えばどのコンストラクタから作られたとかだと判定できないので Date だけをというのは無理ぽいですね。
size 関数
Lazy('hello').size(); // -> 5
興味深いのは sequence のサイズを取るので文字列の sequence であれば文字列長を取れるというところですね。
ここで紹介していないその他の関数はほぼほぼ underscore.js にも同様に存在します。
違うのはだいたいの関数が sequence を返すというところですね。というところで、 Sequence は終わり。
ArrayLikeSequence
define 関数
Lazy.ArrayLikeSequence.define("double", { get: function(i) { var orgObj = this.parent.get(i); return orgObj * 2; } }); console.log(Lazy([1, 2, 3, 4, 5, 6]).double().toArray()); // -> [2, 4, 6, 8, 10, 12]
define 関数は新しい ArrayLikeSequence を継承した sequence を返します。他の define 系でもそうだけど、 MUST でオーバーライドしないといけないものがあって、 ArrayLikeSequence の場合は get 関数をオーバーライドする必要があります。 get して返す前にゴニョゴニョすることによって新しい sequence としてのアイデンティティを作るイメージですね。例では全ての要素を 2 倍するようにしてみたけど、これは数字だけじゃないとダメなので、ちょっと改良しないと実用には耐えないですね。
reverse 関数
console.log(Lazy([1,2,3].reverse().toArray()); // -> [3,2,1]
まんまです。
んー、わりと名前から分かる素直なものしかない上に特に特別なものはなさそうですね。
ObjectLikeSequence
defaults 関数
var people = [ {first: 'foo1', last: 'bar1'}, {first: 'foo2', last: 'bar2'}, {first: 'foo3', last: 'bar3'} ]; Lazy(people).map(function(p){ return Lazy(p).defaults({fullname: p.first + p.last}); }).each(function(p){ console.log(p.get('fullname')); });
あまりいい例ではないけど、オブジェクトのシーケンスに対してデフォルト値を足せるという関数。
functions 関数
var someProduct = { name: 'foo', price: 100, taxIncludedPrice: function(){ return this.price * 1.05; } }; console.log(Lazy(someProduct).functions().toArray()); // -> ["taxIncludedPrice"]
function だけの名前を取りたいときに便利。
invert 関数
var countries = { 'Japan': 'JP', 'United Kingdom': 'GB' }; console.log(Lazy(countries).invert().toObject()); // -> {JP: "Japan", GB: "United Kingdom"}
たまに欲しくなる。
merge 関数
var who = Lazy({firstName: 'foo'}). merge({lastName: 'bar'}, {age: 10}); console.log(who.toObject()); // -> {firstName: "foo", lastName: "bar", age: 10}
直感的な関数。だけど、第2引数にマージ用の関数を指定できたりと多彩。
あとドキュメント読んで欲しいけど、 undefined だったらオーバーライドしない、などといった微妙なルールがあるから気をつけないといけない。
watch 関数
var changes = [], someone = { firstName: 'foo', lastName: 'bar', age: '21' }; Lazy(someone).watch(['firstName', 'lastName']). each(function(change){ changes.push(change); }); someone.firstName = 'hoge'; someone.lastName = 'fuga'; console.log(changes); // -> [{property: 'firstName', value: 'hoge'}, {property: 'lastName, value: 'fuga'}]
特定のオブジェクトの特定の要素の変更をキャッチ出来る、使い道なんだろう。たまに欲しくなりそうだけど、要はフック用って感じかな。
StringLikeSequence
文字列のシーケンスで JavaScript の String にあるような関数群が揃っているけど、返す値は新しい StringLikeSequence といったところで非破壊的ですね。
mapString 関数
var upcase = function(c){ return c.toUpperCase(); }; console.log(Lazy('hello, world').mapString(upcase).toString()); // -> HELLO, WORLD console.log(Lazy('hello, world').map(upcase).toString()); // -> H,E,L,L,O,,, ,W,O,R,L,D
なんで普通の map じゃダメなんだろうっていうのは返り値の型に違いがあるんですね。 mapString の場合 StringLikeSequence を返すので、その後も StringLikeSequence に対する操作を続行出来ますが、 map の場合は sequence が帰るのでダメです。
あまり他に面白いのはないですね、 starts(ends)With もそんなに不思議じゃないので特別感はないですが、コードを簡素に書けそうでとてもいい。特定の prefix から始まっているときは〜 っていう処理はたまに書きたくなりますからね。メタプログラミングとかするのに必要。
GeneratedSequence
each と length があって無限に続くジェネレーターの場合 each を使う前に take 使うっていうのだけ気をつければ良いのではないでしょうか。
AsyncSequence
このライブラリで一番おもしろいところ、というか、ここ近年で非同期処理対する期待とかってどんどん大きくなっていると思うんですよね。だから、なんというか面白いというか、使い方覚えればちゃんと簡単に非同期を扱えるようになるいい感じのやつ。ちなみに幾つかの Sequence 型は async 関数をサポートしていないので気をつけてください。
まず返り値についてですが、 AsyncSequence では直接値を返すような関数を他の Sequence と同じように扱えません。返ってくるのは AsyncHandle というオブジェクトです。基本的にはこの AsyncHandle に対して処理終了後の処理を登録して用います。
var asyncHandle = Lazy.range(100).async(5). reduce(function(n, memo){ return n+memo; }, 0); asyncHandle.then(function(result){ console.log(result); // -> 4950 });
直接的に値を返さない関数(map, filter, など)に関しては他の Sequence 同様に Sequence を返します。これはイントロダクション中に書いてあるコードです。
var asyncSequence = Lazy(array) .async(100) // specifies a 100-millisecond interval between each element .map(inc) .filter(isEven) .take(20); // This function returns immediately and begins iterating over the sequence asynchronously. asyncSequence.each(function(e) { console.log(new Date().getMilliseconds() + ": " + e); });
ただ、この辺挙動が怪しいです。例えば次のようなコードは正常に動作しません。
var asyncSeqs = Lazy.range(100).async(5).map(function(n){ return n * n; }); console.log(asyncSeqs.toArray()); // -> []
一応なんとなく理由は分かるんですが、十中八九バグだと思います。
といったところで幾つかの関数を紹介しましょう。
contains 関数
var asyncSeqs = Lazy.range(1000).async(5); asyncSeqs.contains(500).then(function(result){ console.log(result); // -> true });
この関数が返す AsyncHandle は then 関数を持っているので promise のインターフェイスライクに使えます。
each 関数
var handle = Lazy.range(100).async(100).each(function(n){ console.log(n); }); setTimeout(function(){ handle.cancel(); }, 2000);
基本的には each 関数なので副作用でゴニョゴニョする感じなんですが、一応 AsyncHandle 取れるので途中で処理中断もできます。
他にも幾つかありますが基本的には同じです。
Iterator
これは他の Sequence の define 関数の中でオーバーライドしたりするときに主に使います。基本的にそれ以外では使いません。
ので、説明は略。
AsyncHandle
これは AsyncSequence のとこで出てきたのでもういいですね。
最後に
思いの外これ書くのに時間かかってる*4のでもう疲れてます。基本的に余程大きなデータを扱ったりしない限り underscore.js や Lo-Dash などと大差ないです。それから、まだ安定バージョンじゃないのでバグはあると思います。ほとんど綺麗に動いているように見えますが、何があるか分からないので今後に期待ですね。
追記
書いたあとに issues とか読むためにリポジトリ覗いてて気付いたけど、最近はあまり開発が進められていないみたいですね。だから、無難に Lo-Dash とか使う方がいいと思います。ただ、まぁ fork するなりして PR 送るのもありだと思います。 幾つかのライブラリを組み合わせればこのライブラリと同等のことを実現することは可能だと思いますが、これだけ簡単に面白いこと出来るのはなかなかないのかなーと。