目的
- AngularJS の FRP な(と勝手に思っている)実装の仕組みを理解するために Javascript の FRP ライブラリ BaconJS を触ってみる.
- 以前のブログに書いた記事の見直し.
FRP(Functional Reactive Programing) とは
wikipedia で調べると...
- behaviors と呼ばれる連続的に変化する値をモデリングする.
- 離散的な時間の各点における events をモデリングする.
- events に反応してシステムは変化する.
- 値はモデルからサンプリングした値として評価される.
behaviors は時間と置き換えると良さそう. 時間と言っても離散的な時間の点からなる連続的な値. events は時間の各点から評価された値からなる連続的な値?
GUI を表現する場合を考える. 時間経過に伴う GUI の変化, またユーザのアクションによる GUI の変化,
これらの変化をまとめて events とする. events はその地点の時間と何かしらの任意の引数から評価された値.
FRP ライブラリ Baconjs を試してみる
ボタンをクリックするというイベントが発火したら何か処理を実行する場合を考える
通常通り書く
var button = $('#button');
button.on('click', function(){
alert('you clicked the #button element');
});
指定した DOM にクリックというイベントが発火したら callback の関数を呼ぶ, 直感的でわかりやすい.
Baconjs を使って書く
var cliks = $('#button').asEventStream('click');
cliks.onValue(function() {
alert('you clicked the #button element');
});
まず指定の DOM 上でクリックするというイベント自体を cliks というストリームに定義する.
そうするとこのストリームに定義されたイベントを常時監視できるようになり, イベントが発火した時は onValue の callback 関数が呼び出される.
イベントストリームをひとつのハンドラにまとめる
#plus
という DOM をクリックすると 1 を出力, #minus
という DOM をクリックすると -1 を出力するよう実装する.
通常通り書く
var plus = $('#plus');
plus.on('click', function(){
console.info(1); // 1
});
var minus = $('#minus');
minus.on('click', function(){
console.info(-1); // -1
});
Baconjs を使って書く
var plus = $('#plus').asEventStream('click').map(1);
var minus = $('#minus').asEventStream('click').map(-1);
var both = plus.merge(minus);
both.onValue((val), function() {
console.info(val); // 1 または -1 を出力
});
plus, minus にそれぞれ定義したストリームを both というストリームでまとめる(マージ).
登録したイベントを監視し, 発火時には map により指定した値を引数に返す callback 関数を呼び出す.
イベントを発火させる DOM が増える場合などこのようにイベントをひとつのストリームに入れておいて, ひとつのハンドラで全てのイベントを監視すると簡潔な記述になって良さそう.
イベントのストリームと現在の値を示すプロパティ値を使う
ここから BaconJS の本領を垣間見る.
通常通り書く
var sum = 0;
plus.on('click', function() {
sum += 1;
$('#sum').text(sum);
});
minus.on('click', function() {
sum -= 1;
$('#sum').text(sum);
});
カウンターを実装する.
さて... sum への代入... 通常通り書くと参照透過性が崩れる.
そして $('#sum').text(sum)
が重複していてなんだか気持ち悪い.
Baconjs を使って書く
var counter = both.scan(0, function(x, y) {
return x + y;
});
counter.onValue(function(sum) {
$('#sum').text(sum);
});
わっほい. 簡潔で素敵.
イベントのストリームから scan メソッドを使うと, このストリームでの値を使った現在の値を持つことができるっぽい. 現在の値というところがポイント. イベントのストリームは離散的にそのイベントごとに対応する値を, scan の引数の第一項は初期値, そして第二項は現在の値とストリームから得た値を引数に取るクロージャとなるみたい.
今回のチュートリアルの例では, イベントのストリームが behaviors, イベントのストリームのある地点の値とカウンター値からなる連続的な値が events という認識で作った. 通常通り書くと sum の値は plus と minus または他の手続きなど, 複数の手続きから代入されるので値の状態の保持に一貫性を持たせるのが難しい気がする. (いつだれがどう値を書き換えたか見つけにくい)
一方 Baconjs で記述すると onValue が常にイベントを監視してイベント発火時のみに現在の値を目的の処理に適応するのがわかりやすいので, 個人的には結構気に入った.
Baconjs を使ってみてわかったこと
- イベントをストリームという離散的な集合としてとらえこれに含まれるイベントを一括で監視する.
- イベント発火時の callback 関数または返す値とは別に可変する現在における状態を保持するインターフェイスを組み合わせることで参照透過な値をいい感じにラップしてくれる.
- 参照透過性を保ちかつ簡潔な記述が可能っぽい.
※ 追記
テキストはちょこちょこ修正していく.
次は AngularJS さんを読んでいこうかな.