Posted at

Bacon.jsのFunction construction rules

More than 5 years have passed since last update.

基本的には一次資料の雑なパラフレーズ。コードはBacon.jsの他にbacon.jqueryも利用している。


Function construction rulesとは

Bacon.Observableが持つonValue()やmap()のようなメソッドは、引数としてハンドラ関数を受け取り自身に登録する。この際、以下のような単純な関数の指定だけではなく

stream.onValue(function(arg) { /* ... */ });

色々な指定方法が一定のルールに従って許容されており、このルールがBacon.jsのドキュメントで'Function construction rules'と呼ばれている。


通常のハンドラ指定と引数の部分適用

上記の通り、onValue()などは第一引数としてハンドラ関数を取り得る。ハンドラ関数の引数にはストリームが現在持っている値が渡されるが、onValue()などの引数としてハンドラ以降に更に他の値が渡されている場合、それらが先にハンドラに渡されることになる。

ストリームの現在の値は、常にハンドラの最後の引数として渡される。

<div>

<input id="echo">
<input id="hello">
<input id="yo">
</div>

function greeting(greet, name) {

console.log(greet + ', ' + name + '!');
}

// 普通にハンドラ関数を設定する
Bacon.$.textFieldValue($('#echo')).onValue(function(name) {
console.log(name);
});

// onValue()時にgreeting()を設定すると同時に第一引数を束縛しているので、
// #helloに入力を行うと'hello, ****'、#yoに入力を行うと'yo, ****'と
// 出力される
Bacon.$.textFieldValue($('#hello')).onValue(greeting, 'hello');
Bacon.$.textFieldValue($('#yo')).onValue(greeting, 'yo');

Bacon.$.textFieldValue()は、大雑把にはinputのjQueryオブジェクトが入力される度にinputのstringが現在値として流れるストリームのBacon.Propertyを生成するものと考えればよい。


オブジェクトのメソッドをハンドラとして指定する

onValue()などは、これまで見てきたような関数だけでなくオブジェクトも第一引数として取り得る。この場合、第二引数にメソッド名として文字列を取り、第一引数に渡されたオブジェクトについてその文字列を名前とするメソッドをハンドラとする。

メソッド名以降の引数の適用ルールは通常の指定のルールに準ずる。ちなみに、文字列で指定したメソッドが無い場合はTypeErrorが発生する(apply()しようとしているpropertyがundefined)。

登録画面か何かで、パスワードが8文字より短い場合ボタンを押せないようにするようなケースを考える。

<div>

<input type="email" id="email">
<input type="password" id="password">
<button type="button" id="button" disabled>
</div>

function isTooShort(minLength, txt) {

return txt.length < minLength;
}

Bacon.$.textFieldValue($('#password'))
.map(isTooShort, 8)
.changes()
.assign($('#button'), 'prop', 'disabled');

上記の例は、ストリームの値として流れてきた'#password'に入力された文字列をmapで「8文字より短いかどうか」のbooleanに変換し、assign()(onValue()のエイリアス)で'#button'のprop()に第二引数として渡すよう指定している(第一引数は'disabled')。

changes()は、入力される度にストリームが流れるのではなくtrue/falseが切り替わった時のみ下流に流れるようにすることで不必要な状態の変化を防いでいる。

上の例は通常の指定の仕方で更に全てまとめてしまうなら以下のような書き方もできるが、スコープの限定された問題解決を組み合わせて大きい問題を解決していく方がより関数型プログラミングらしいと思われるので、場合にもよるが上記の方が望ましいケースが多いだろう。

この場合、changes()を挟むことができないのもデメリットと言える。

function disable(el, minLength, txt) {

el.prop('disabled', txt.length < minLength);
}

Bacon.$.textFieldValue($('#password'))
.onValue(disable, $('#button'), 8);

またこの時、以下のようにオブジェクトのメソッドを直接指定するとどうなるかというと、

Bacon.$.textFieldValue($('#password'))

.map(isTooShort, 8)
.changes()
.assign($('#button').prop, 'disabled');

通常のハンドラ指定として処理されるためprop()内でthisが束縛されず、意図通りに動作しないだろう。

逆に言えば、thisを内部で用いていない関数であればオブジェクトのメソッドだろうがなんだろうが普通に指定すればよいので、例えば一番最初の例などは以下のように書くことができる。

Bacon.$.textFieldValue($('#echo')).assign(console.log);


余談:assignとonValue

Bacon.jsのコードを読めば自明だが、assign()は完全に単なるonValue()の別名である。コードの可読性のために規約的に使い分けるあるいは片方を一切使わなくてもよいと思うが、業務で使う際は


onValue

ハンドラ関数をベタに指定する場合

assign

オブジェクトメソッドを指定する場合

という風に使い分けることで読み下す時の違和感を減らすようにしている。というかonValue()はon()で良かったのではないかという気がするが多分onValues()的にそういうわけにもいかなかったのだろうという気もしている。


オブジェクトのメソッド指定と可変長引数

オブジェクトのメソッドをハンドラとして指定した場合好むと好まざるに関わらずストリームの値は末尾の引数として渡されてしまうため、例えば指定したメソッドが可変長引数として実装されている場合、意図しない値が引数として渡されないよう注意が必要である。

例えば、上記の例で結果メッセージの表示を考えてみる。

<div>

<input type="email" id="email">
<input type="password" id="password">
<button type="button" id="button" disabled>
</div>
<div id="message" style="display:none">
<p>succeeded.</p>
</dv>

$('#button').clickE()

.map(function() { /* 何か値を返す処理 */ })
.assign($('#message'), show, void 0);

assign()の最後のvoid 0が無い場合、メッセージの表示に恐らく意図しないトランジションが伴う。

void 0が無い場合ストリームの現在値がjQuery.show()に渡されてしまうので、無引数とは異なりトランジションを伴う処理として呼び出されてしまう。この場合別にvoid 0はnullでもいいが、指定されているハンドラの実装による。

尤も、いちいち面倒くさいし格好良いものでもないので、jQueryオブジェクトを受け取りshowを無引数で呼ぶようなラッパーを実装し、jQuery.show()を直接呼ぶのでなくそれを用いるようにしてもいいだろう。


プロパティアクセス

onValue()などは、関数やオブジェクトだけでなく"."で始まる文字列も第一引数として取り得る。この場合、ストリームを流れてくる値はその文字列名のフィールドを持っていることが求められ、フィールドが単なる値であればその値が、関数であればその関数の結果がストリームの値として下流に流れていく。

上記の振る舞いから、通常map()で使われるケースが殆どだろう。プロパティアクセスの手法を用いると、先の例は以下のように書き換えることができる。

function lt(y, x) {

return x < y;
}

Bacon.$.textFieldValue($('#password'))
.map('.length')
.map(lt, 8)
.changes()
.assign($('#button'), 'prop', 'disabled');

一つ目のmap()でストリームの値を文字列からその長さに変換し、二つ目のmap()でより汎用的に実装された比較関数で長さからbooleanに変換されている。

一次資料ではプロパティアクセスのネストやフィールドが配列であった場合の記法についても解説されているが、この記法の応用であり比較的自明でもあるので割愛する。


定数とBacon.Propertyのmapへの適用

上記のあらゆるルールに当てはまらない場合、onValue()などに渡した第一引数がそのままストリームの値として下流に流れていく。

これも、その振る舞いからmap()で使われるケースが殆どだろう。

ルールの話から若干逸脱するが、map()にBacon.Propertyを指定するとPropertyオブジェクトそのものでなくPropertyの現在値が下流に流れる値として用いられる。

$('#button')

.clickE()
.map(Bacon.combineTemplate({
type: 'POST',
url: '/signup',
data: {
email: Bacon.$.textFieldValue($('#email')),
password: Bacon.$.textFieldValue($('#password'))
}
})
.ajax();

若干応用的なコードだが、Bacon.combineTemplate()は渡したテンプレートオブジェクトに従ってBacon.Propertyを生成するものであり、Bacon.Observable.ajax()は流れてきたストリームの値を引数としてjQuery.ajax()を呼びその結果を下流に流すmap()の亜種のようなものと考えればよい。

この場合は、Bacon.combineTemplate()の結果のPropertyがmapに渡されているからといってそのままストリームの値として下流に流れていくのではなく、そのPropertyの現在値として生成されるオブジェクト(data.emailとdata.passwordが、それぞれのフォームによって生成されたストリームの現在値に変換される)が流れていく。


終わり

ケースに応じて適切なハンドラ指定を利用し、良いBacon.jsライフを。