Reactive Extensions を使ってコナミコマンドを実装する


2011年 12月 16日

問題

Reactive Extensions で非同期処理を簡潔に記述するでは「キー入力に応じて補完候補を表示する」「ただし補完候補はAjaxで非同期に取得する」という
いまどきのWebアプリケーションにならあって当然の機能が、Reactive Extensions (Rx)を使うことであたかも普通のリスト処理のように記述できることを示しました。

入力補完は例としては単純で分かり易いものの、
もう少し別の例も欲しいところです。
という訳で、 Rx を使うことでコナミコマンドを実装してみましょう。

回答1: Rx を使わない場合

var lastKeyCodes = [];
var validKeyCodes = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
$(window).keydown(function (e) {
  lastKeyCodes.push(e.keyCode);
  if (validKeyCodes.length < lastKeyCodes.length)
    lastKeyCodes.shift();
  if (lastKeyCodes.toString() == validKeyCodes.toString())
    alert('***!');
});

「最新のキー入力10個がコナミコマンドに合致するなら」という考えで書いてみました。
まあこんなものでしょう。

回答2: Rx をなんとなく使った場合

var lastKeyCodes = [];
var validKeyCodes = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
var konamiStream =
  $(window)
  .toObservable('keydown')
  .Select(function (e) {return e.keyCode;});
  .Select(function (c) {
    lastKeyCodes.push(c);
    if (validKeyCodes.length < lastKeyCodes.length)
      lastKeyCodes.shift();
    return lastKeyCodes.toString() == validKeyCodes.toString();
  });
konamiStream.Subscribe(function (b) {
  if (b)
    alert('***!');
});

回答1を直訳しました。
キー入力の処理と実際に行いたい処理を分離できているという点では良いのですが、
toObservable だの Subscribe だののために若干記述が冗長になっています。

回答3: Rx を華麗に使った場合

var validKeyCodes = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
var konamiStream =
  $(window)
  .toObservable('keydown')
  .Select(function (e) {return e.keyCode;});
  .BufferWithCount(validKeyCodes.length, 1)
  .Select(function (cs) {
    return cs.toString() == validKeyCodes.toString();
  });
konamiStream.Subscribe(function (b) {
  if (b)
    $('#result').prepend('<div>' + b + '</div>');
});

回答1と回答2の真の問題点は最新のキー入力(lastKeyCodes)という状態を管理していることです。
そのために本来やりたいこと(コナミコマンドの入力判定)とは関係のないものが途中に混ざってしまい、
回りくどいコードになっています。

しかし
BufferWithCount (C# では Buffer)
を使えば明示的に状態を管理することなく「最新のデータn個」単位で処理を記述することができます。
これで本来やりたいことをすっきりと記述できるようになりました。
やりましたね。

補足

BufferWithCount の最初の引数には一度に処理したいデータの個数を指定し、
二番目の引数では各処理の間でスキップするデータの個数を指定します。
今回の例の場合は BufferWithCount(validKeyCodes.length, 1) ですが、
BufferWithCount(3, 3) とすれば「3個単位でデータを処理する」こともできます。

例えば xs = [1, 2, 3, 4, 5, 6, 7, ...] というデータのシーケンスがあったとすると、

  • xs.BufferWithCount(3, 1)
    [[1, 2, 3], [2, 3, 4], ...]
  • xs.BufferWithCount(3, 3)
    [[1, 2, 3], [4, 5, 6], ...]

というシーケンスに変換できるということです。

Rx には他にも便利なメソッドが山のようにあるので、
これを駆使すればより複雑な処理も簡潔に書けるようになるでしょう。