継続渡しによる停止・再開可能な反復処理
ここ最近の 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
ジェネレータの性質上、一方向の再生しかできませんが、実装を頑張れば、前ページへの移動等も可能になると思います。