今時の若者ならばHeroku等を利用して手早く Web アプリを作成・公開することが日常茶飯事です。
バックエンドもフロントエンドも今はフレームワークが充実していますから、
高度な処理を簡潔な記述で行うことができます。
しかし非同期処理となると話は別です。
例えばフロントエンドを作るとなると、まずjQueryを使うことになるでしょう。
jQuery は洗練された API で DOM 操作を簡単に行うことができますし、
非同期通信についても $.ajax
を使えば煩雑なことほぼ知らずに済みます。
例えばWikipediaの検索フォームは入力補完が行われるようになっており、
検索フォーム文字が入力されると関連するページのタイトルが候補として表示されます。
このような処理を書くとなると以下のようなコードになるでしょう:
var showCompletionMenu = function (words) {
...
};
var completeWords = function (partialWord) {
$.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
limit: 100,
search: 'foo',
success: function (data) {
var words = data.data[1];
showCompletionMenu(words);
}
}
});
};
var $form = $('#userInput');
$form.keyup(function () {
completeWords($form.val());
});
このように、イベント処理や非同期通信を行う場合、
$form.keyup
)と発生したイベントを処理する部分(completeWords
)$.ajax
)と受信した結果を処理する部分(showCompletionMenu
)を分離し、前者にコールバック関数として後者を渡す形になります。
普通のプログラムならば上から下へ順次処理が行われるのですが、
このように非同期処理が絡むと実行の流れはソースコードの見た目から背離します。
この単純な例ですら非同期処理が2段も積み上がっており、
とても反射的に読んで意味を理解できるソースコードではありません。
ここで本当に行いたいことは補完候補の表示であって、
その表示タイミングや補完候補のデータの出所は重要ではありません。
しかしコールバックという名の中間層を噛まさざるを得ないため、
本質からやや遠ざかったソースコードになっています。
どうにかしてこの状況を打破できないでしょうか。
Reactive Extensions (Rx) を使います。
Rx を使えば非同期なデータ処理を簡単に記述し、また合成することができます。
Rx は .NET Framework 上の1ライブラリなのですが、
JavaScript 版の Rx も提供されています。
試しに先程の問題の例を Rx を使って書くと以下のようなコードになります
(実際に動作するサンプル):
var showCompletionMenu = function (words) {
....
};
var completeWords = function (partialWord) {
return $.ajaxAsObservable({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
search: partialWord,
format: 'json',
limit: 100
}
})
.Select(function (data) {return data.data[1];});
};
var $form = $('#userInput');
var observableWords = $form
.toObservable("keyup")
.Select(function (_) {return $form.val();})
.Select(function (partialWord) {return completeWords(partialWord);})
.Switch();
observableWords.Subscribe(showCompletionMenu);
ここで注目すべきは observableWords
の定義です。
Rx では非同期に得られるデータをあたかも普通のデータのシーケンスであるかのように取り扱うことができ、
普通のリスト処理のようにデータの操作処理を積み重ねていくことができます。
まず $form.toObservable("keyup")
では入力フォーム($form
)で起きたキー入力イベントを
普通のデータのシーケンスであるかのように扱えるようにしています。
例えば keyup
イベントでは「どのキーが入力されたか」等の情報が得られますが、toObservable
を使うことでこのようなキー入力情報のシーケンスが存在するかのように処理を記述することができます。
一旦 toObservable
で変換してしまえば後は任意の処理を積み重ねることができます。
.Select(function (_) {return $form.val();})
jQuery.map
です)。.Select(function (partialWord) {return completeWords(partialWord);})
では.Switch()
completeWords
の結果を合成して「補完候補の単語の配列」のシーケンスに変換しています。Switch
を使うとこのような場合に対して適切な結果を取捨選択して合成してくれます。一度シーケンスができあがってしまえば、
後は .Subscribe
で各要素に対する処理を記述できます(jQuery で言えば jQuery.each
のようなものです)。
ここでは例示のために completeWords
や showCompletionMenu
は一度変数に代入していますが、
やろうと思えばこれは全て無名関数にして .Select
や .Subscribe
の引数に渡すこともできます。
実行時の流れとソースコード上の見た目がほぼ一致する形となり、
Rx を使うことで非常に理解し易い記述ができます。
やりましたね。
しかし上記の例だと単純なので面白くありません。
実用性を考えると色々と調整が必要です。
でも Rx を使えば簡潔に記述することができます。
最初の例ではキー入力の度に Wikipedia にリクエストを送信してしまいます。
実際のユーザーのキー入力を考えると、
個々のキー入力の発生間隔はかなり短い(知っている単語を入力している時など)か
かなり長い(綴りを思い出せなくなった時など)かのどちらかです。
前者の最中に補完候補を求めても無駄になる確率が高いですから、
実用性を考えると後者のタイミングで補完候補を求める方が良いでしょう。
もう少し厳密に言えば
「500ミリ秒以内に連続してキー入力が発生した場合は最後のキー入力のみを使う」
ということになります。
これはよくあるパターンなので Rx に既に API が用意されています。
具体的には observableWords
の定義を1行追加するだけで実装できます:
var observableWords = $form
.toObservable("keyup")
.Select(function (_) {return $form.val();})
.Throttle(500)
.Select(function (partialWord) {return completeWords(partialWord);})
.Switch();
最初の例では、カーソルキーでのカーソル移動など、
入力フォームの内容が変化しない場合であっても補完候補の取得を行っています。
これも無駄ですから、データの変化があった場合だけ補完候補を取得する方が良いでしょう。
これもよくあるパターンなので Rx に既に API が用意されています。
具体的には observableWords
の定義を1行追加するだけで実装できます:
var observableWords = $form
.toObservable("keyup")
.Select(function (_) {return $form.val();})
.Throttle(500)
.DistinctUntilChanged()
.Select(function (partialWord) {return completeWords(partialWord);})
.Switch();