function runnable(f) { var o; o = f(function () { o.next(); }); o.next(); } runnable(function (next) { // 初期化 var answer = Math.floor(Math.random() * 10 + 1); // ボタンが押されるまで yield で待機するよう設定 document.getElementById('guessme_button').onclick = next; // ループ while (1) { yield; var guess = document.getElementById('guessme_value').value; if (guess < answer) { alert('もっと大きいよ'); } else if (guess > answer) { alert('もっと小さいよ'); } else { alert('大正解!'); break; } } // リプレイ document.getElementById('guessme_value').value = ''; runnable(arguments.callee); });

見てのとおり、1-10 の間の数字を当てるゲームです。↓で遊べます。

数字を入力: (1-10)

正直、最初はあまり便利だと思わなかったのですが、ちょっといいかもと思うようになってきました。例えば Firefox の拡張機能で、多数の URL からデータをダウンロードする場合を考えてみます。同期的な処理を行うと、ダウンロードが完了するまで UI をブロックしてしまうため、非同期処理で実装する必要があります。なので、普通はこんな感じで書くことになります。

一般的な書き方 function get_all(urls) { if (urls.length != 0) { // build request var xhr = new XMLHttpRequest(); xhr.open("get", urls.shift(), true); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { // handle response if (xhr.status == 200) { ... } // initiate next request get_all(urls); } }; // send request xhr.send(null); } }

慣れればどうってことはないのですが、再帰的な処理を行う必要があります。

それに対し、yield を使うと以下のように書くことができます。

yield を使った書き方 function get_all(urls) { runnable(function (next) { for (var i = 0; i < urls.length; i++) { // build request var xhr = new XMLHttpRequest(); xhr.open("get", urls[i], true); xhr.onreadystatechange = function () { if (xhr.readyState == 4) next(); }; xhr.send(null); // wait for response yield; // handle response if (xhr.status == 200) { ... } } }); }

再帰呼び出しの必要がなくなりました。すべてのダウンロードが完了するまでスコープの移動がないので、変数の取り回しが楽になります。それにしても協調型の並列処理なんて、もうとっくに墓場送りになっているものだと思っていましたが、まだまだ使う機会があるんですね (笑)

なお、復帰条件の設定と yield の呼び出しが分かれていて気持ち悪いという向きには、yield の引数として条件を渡すという手もあります。yieldをwait代わりに使っちゃっていいのかなー - outsider reflex は、復帰条件を時間経過に限定するかわりに、そうしているようです。