継続渡しによる停止・再開可能な反復処理

ここ最近の Scheme プログラミングで得た感覚を踏まえ、各種 JavaScript ライブラリでお馴染みの $ 関数 (ID による要素検索) を、ごく簡易にですが、関数スタイルで実装してみました。

まずは実装を見ていただく前に、このような例を考えてみてください。

$("id").do_something();

ここで、"id" 要素が確実に見つかるかどうかが保証されていない (do_something が失敗するかもしれない) とします。すると、

var o = $("id");
if (o) {
  o.do_something();
}

のように、いちいちテストをしなければいけませんよね。

これが、関数スタイルでは

$("id", do_something);

と書くだけで済みます。つまり、$ 関数自身が成否のチェックを行い、成功していればコールバック関数にオブジェクトを渡すように設計してやるわけです。

このように実装しました:

function identity(x) { return x }

function maybe_yield(o, k) {
  return o && (k||identity)(o);
}

function $(id, k) {
  return maybe_yield(document.getElementById(id), k);
}

maybe_yield という関数がポイントになります。これの意図するところは、コールバック関数 k が与えられなかった場合は値をそのまま返してほしい、ということです。すなわち、OO スタイル、関数スタイルどちらの書き方も許す柔軟性が得られるわけです。


maybe_yield 関数の応用として、ジェネレータのようなものを作ることもできます。

function ary2gen(ary) {
  var gen = function() {
    return (ary.length > 0) ? ary.shift() : false;
  };
  return function(k) {
    return maybe_yield(gen(), k);
  };
}

利用例:

var g = ary2gen([1,2,3]);
g(alert); // 1
two = g();
g(alert); // 3
g(alert); // nothing happens


さて本題ですが、このジェネレータ関数に基づいて、中断・再開が可能なイテレーション関数を考えてみました。

function iter_items(upto, generate, f, k) {
  var loop = function(i) {
    generate(function(item) {
      f(item, i);
      if (i < upto) {
        loop(i+1);
      } else {
        // Pass continuation to `k'
        k(function() { loop(1) });
      }
    });
  };
  loop(1);
}

例として LDR のフィード表示のようなものをイメージしてください。upto は一度に表示する件数を制限するものです。generate が上述のジェネレータで、記事データを一件ずつ出力します。

f は画面に記事を描画する関数にあたります。

では k はと言いますと、次ページを表示するボタンのようなものです。これにはループ処理を再開するための関数を渡します。

簡単な呼び出し例:

iter_items(2, ary2gen([1,2,3]), alert, function(resume) {
  confirm("proceed?") && resume();
});

サンプルを作ってみましたのでご覧下さい: generated-news.html

ジェネレータの性質上、一方向の再生しかできませんが、実装を頑張れば、前ページへの移動等も可能になると思います。