何が問題か
Bacon.jsで何らかの実装をする時、同期的なAPIのライブラリなどなら普通にsubscriberなどの中で使ってストリームに流していけばいいのだが、非同期処理のためのコールバックベースなAPIの場合Bacon.Observable.onValueやmapなどの基本的な利用だけではうまく組み込むことができない。
例えば、File APIのFileReaderはBacon.jsでどうやって扱えばよいか?
指針
Bacon.jsで何か困ったら、基本的には「やろうとしていることを、どうやってストリーム(とプロパティ)という概念で解決することができるか」と考えると考えやすい。やりたいことを逐次的に実行し相互に呼び合うような処理を実装するというよりも、常に、入力―変換―出力(―消費)を行うストリームを定義していきそれを接続していくような考え方になる。
なので、きれいに書かれたBacon.jsのコードは処理の記述というよりは配線図に近い印象になる。あるいはピタゴラ装置。
FileReaderであれば、「ファイル名を入力としてファイルの内容を出力するストリーム」を定義すればよさそうだ。では、コールバックベースのAPIを利用してBacon.jsでどうすればそれを実現できるのか?
Bacon.fromCallback
Bacon.jsは、色々なシチュエーションに応じて新しくストリームを生成するfrom***系のAPIを提供している。端的に言って、大体コールバックベースのAPIの場合はBacon.fromCallbackを利用すれば問題を解決できるだろう。
例えば、FileReaderとBacon.fromCallbackを用いて「ファイル名を入力としてファイルの内容を出力するストリーム」の最低限の定義をするなら以下のような実装が考えられるだろう。やっていることは、Promise系の実装からのキャッチアップとほぼ同じになる。
function readFile(fileName) {
return Bacon.fromCallback(function(callback) {
var reader = new FileReader();
reader.onload = function() {
callback(reader.result);
};
reader.onerror = function() {
callback(new Bacon.Error(reader.error));
};
reader.readAsText(fileName);
});
}
readFileは、fileNameで指定されたファイルの内容を出力するストリームを生成して返す。Bacon.fromCallbackの指定した関数の引数であるcallbackは、このreadFileが生成したストリームのsubscriberとして指定(つまりBacon.Observable.onValueやmapなどに指定)された関数と考えればよい。読み込みに失敗した場合のcallbackは、Bacon.Observable.onErrorやmapErrorなど異常系のsubscriberが実行される。
これでFileReaderはBacon.jsのコンテキストで使えるようになったので、例えば以下のようにシームレスに組み込むことができるようになる。
var fileSelectionStream = $('fileSelection')
.asEventStream('change')
.map('.target.files.0')
.flatMap(readFile);
fileSelectionStream.onValue(function(contents) {
// something about file contents...
});
fileSelectionStream.onError(function(e) {
// error handling...
};
終わり
Bacon.jsを用いてあらゆるものをストリーム化しましょう。