Bacon.jsで眺めるFunctional reactive programming

JavaScriptのFunctional reactive programming(FRP)ライブラリ「Baconjs」を使ってFRPの考え方を勉強してみたメモ。

なお、このメモはFRPの勉強のためにいくつかの資料をざっくりまとめただけなので、Bacon.jsの使い方は解説しない。
なのでBacon.jsの使い方を知りたいだけなら元記事を読んだほうが良い。

三行で

  1. FRPでは従来のプログラミングでは間接的にしか扱えなかった「徐々に変化する値」を第一級のオブジェクトとして扱うことができる。
  2. さらに関数型のcompositionalな性質が合わさって素敵。
  3. Bacon.jsは良いものだ。

従来のプログラミングでは時間とともに変化する値の扱いが難しい

例えば次のフォームの例を考えてみる。

<form>
  <input type="text" id="username" name="username" placeholder="ユーザー名"/>
  <input type="text" id="fullname" name="fullname" placehodler="フルネーム"/>
  <button id="registerButton" disabled="disabled">登録</button>
</form>

このフォームに登録ボタンを押すとユーザー名とフルネームを送信する処理を実装してみる。

registerButton.click(function(event) {
    event.preventDefault();
    var data = { username: usernameField.val(), fullname: fullnameField.val()};
    $.ajax({
        type: "post",
        url: "/register",
        data: JSON.stringify(data)
    });
})

楽勝だ。
しかしこれではちょっと実用に耐えない。もう少し機能を追加してみる。

  1. ユーザー名が空の場合は送信ボタンを無効化する
  2. フルネームが空の場合は送信ボタンを無効化する
  3. ユーザー名が一文字入力されるごとに重複した名前があるかどうかをチェックする
  4. ユーザー名が重複していれば送信ボタンを無効化する

↓実装後

var usernameAvailable, checkingAvailability;
var usernameField = $('#username');
var fullnameField = $('#fullname');
var registerButton = $('#registerButton');
usernameField.keyup(function(){
    $.ajax({ url : "/usernameavailable/" + username}).done(function(available) {
        usernameAvailable = available;
        updateButtonState();
    });
});

registerButton.click(function(){
    updateButtonState();
    var data = {username: usernameField.val()};
    $.ajax({
        type: "post",
        url: "/register",
        data: JSON.stringify(data)
    });
});

var updateButtonState = function(){
    setEnabled(registerButton, usernameavailable
                               && nonEmpty(usernameField.val())
                               && nonEmpty(fullnameField.val())
                               && !checkingAvailability);
};

たった2つの入力項目をもつフォームのはずが、やや複雑になってしまった。
この調子でさらに

  1. ajax通信中はインジケーターを表示する
  2. 登録処理中はボタンを無効化する

といった処理を追加していくと、誰も保守できないコードになっていく。

このコードを改善するにはどうすればいいのか。
たしかにMVCオブジェクト指向を使ってコードをある程度改善することはできる。しかし、複雑さの根本には時間の経過(例えばユーザー名が入力によって少しずつ変化していくこと)が第一級のオブジェクトとして扱えないことにある。オブジェクト指向では状態とその変更という手段では時間経過を間接的にしか表現できないことが複雑さの原因だ。

FRPは「時間の経過を表現するデータ型」を提供する

FRPは徐々に変化する値(上の例ではユーザーの入力によって変化していくテキストフィールドの値)を第一級のオブジェクトとして扱うことができる。

さきほどの例をFRPライブラリのBacon.jsを使って書き換えていく。
まず、「入力によって変化していくユーザー名」を表現すると次のようになる。

var username = $('#username').asEventStream("keyup").map(function(event) { return $(event.target).val(); }).toProperty("");

このような値をFRPでは「振る舞い」と呼ぶ。
例えばユーザー名が変化するごとに内容をconsoleに出力するコードはこんな感じだ。

username.onValue(function(value){console.log(value);});

これを実行するとこうなる。

h
he
hel
hell
hello
hellow
hellowo
hellowor
helloworl
helloworld 

この程度なら従来の方法を使って同じことができると考えるかもしれない。

$('#username').keyup(function(e){
  console.log($(this).val());
}

FRPの利点は振る舞いを自由に加工したり、貼りあわせたりして新しい振る舞いを定義できることにある。
例えばユーザー名が空ではないことを表す振る舞いは以下のコードで表現できる。

var nonEmpty = function(x){ return x.length > 0;};
var usernameEntered = username.map(nonEmpty);

イベントを柔軟に扱えるようになった。

  1. ユーザー名が空の場合は送信ボタンを無効化する
  2. フルネームが空の場合は送信ボタンを無効化する

を実装すると

//usernameEnteredとほとんど同じ
fullnameEntered = fullname.map(nonEmpty);
buttonEnabled = usernameEntered.and(fullnameEntered);

//小さな振る舞いを組み立てていくことで副作用の部分を一箇所にまとめることができる
buttonEnabled.onValue(function(enabled){
  $("registerButton").attr("disabled", !enabled);
});

複雑な条件を簡潔に表現できて良い

疲れてきたので適当になるけど、FRPではajaxも同じ枠組みで扱うことができる。

var availabilityResponse = username.changes().map(function(user) { return { url: "/usernameavailable/" + user };}).ajax();
var usernameAvailable = availabilityResponse.toProperty(true);

//updatebuttonstateと同等の処理が次のようにかける!
var buttonEnabled = usernameEntered.and(fullnameEnterd).and(usernameAvailable);

ミュータブルな状態がなく小さなものから組み立てているので最初の実装例に比べて簡潔で拡張性が高い。

Bacon.jsすごく良い

Bacon.jsはjQueryプラグインとしても提供されているので既存のプロジェクトに混ぜるのも楽そう。
TypeScriptと組み合わせるともっと幸せになれそう。

FRPのテストについてとか、もっと大規模なアプリではどう構造化するべきなのかについても調べて後で書きたい