JavaScriptのyield
2010/05/08(土) 23:02 Javascript親記事へこのエントリーをはてなブックマークに追加

いわゆるジェネレーターというものについて
yieldのsleep実装。他のブラウザでも使うフレームワーク
使えるのはFirefox限定?

実行するにはJS1.7をあつかえるものが必要になります。
Firebugは対応してないので、xqjs - ’ellaneousを使うと便利です。

*注
"→"は"つまり"程度の意味で読んどいてください。

MDCのジェネレーターで扱われているフィボナッチ数列のyieldを例にする。
function fib() {
  var i = 0, j = 1;
  while (true) {
    yield i;// 毎回ここで止まる
    var t = i;
    i = j;
    j += t;
  }
}
// *1
var g = fib();
for (var i = 0; i < 10; i++) {
// *2
  p(g.next());
}
*1
fib()なので、gに入るのはfibの実行結果。
通常ならreturnだが、yieldキーワードがfib内に存在する。
yield キーワードを含む関数(fib)はジェネレータという。
ジェネレーターを実行するとfib中身が実行されることなく、ジェネレータオブジェクト(ジェネレータ・イテレータ)という特殊なオブジェクトが返ってくる。(まだvar i=0なども起きてない)
→ まだ何も処理は行われてない
→ 返ってくるのはジェネレーターオブジェクト
*2
g.next()でyieldのところまで実行している。
yield i;はreturn i;と読み換えて(イメージ的)、返すのはi=0なので0、そしてそこで処理が止まる。
これで一回目のnext()は終わり。
次にg.next()すると、var t = i;から始まってwhileループしてるのでまたyieldのところで止まり、iの値を返す。
(g = g.next();としなくても、gは幾つ進んでるのを覚えている。)

つまり、yieldは渡された引数の値(iの事)を返して、同時にその行で処理を一時停止している。

ジェネレータオブジェクトは以下のメソッドを持っている

  1. next()
  2. send(n)
  3. close()
send()は引数なしだとnext()と等価だが、作ったジェネレータを一度next()してからでないとsend()メソッドを使うことはできない。

追記:
yieldの返り値とsend()の意味合いは次のコードに詰まってる。
function f() {
  p(1);// ... (a)
  var value = yield 2;// ... (b)        
  p(value);// ... (c)
}
var g = f(); // この時点で f は実行されず、ジェネレーターオブジェクトを返す
p(g.next()); // ここで (a) まで実行される→yieldに渡した引数(2の事)をreturnして一時停止
g.send(3); // valueに3をセットして、next()する。
// (b)の次の行から、つまり(c)から実行する。(そしてStopIterationの例外が起きる)
実行結果は1,2,3と出てくる。

yieldを使った非同期処理を同期的に書く方法。

runnableはジェネレータを覆う関数。
runnableの最後にnext();しておかないと、runnableの引数となる関数の仮引数(request)を実行した際には実体となってる関数function (t) { try{ o.send(t);catch(e) }}が実行されるので、sendはエラーになってしまう。
request(t)で実際にやることはo.send(t)になるので、引数に"hoge"としている場合、hogeを返す。
    function runnable(f) {
        var o;
        o = f(function (t) { try{ o.send(t); }catch(e){} });
        o.next();// ジェネレーターオブジェクトを作ったら一回目のnext();
    }
    runnable(function (request){
        // yieldで一時停止        
        var result = yield setTimeout(function () {request("hoge");}, 2000);// ここの内部的な動きがイマイチ
       
        p(result);// 2秒後にprintされる。
    });
もう少し掘り下げてみる。
上のコードは展開すると以下のような形になってる。
var f = function(request){
    // yieldで一時停止        
    var result = yield setTimeout(function () {request("hoge");}, 2000); 
    p(result);// 2秒後にprintされる。
}
var o = f(function(t){ try{ o.send(t); }catch(e){} });// ジェネレーターが返る
o.next();// 停止してたのを動かす→setTimeoutが実行される
o.next();でyieldによって止まっていた関数が動き出すので、setTimeoutが実行されて、
2秒後にrequest("hoge");つまりo.send("hoge");を実行することになる。
o.send("hoge")は resultに"hoge"をセットしてnext()となるので、resultには"hoge"が入り p(result);が実行される。

やっぱりイマイチピンとこないけど便利。

名前:  非公開コメント   

  • TB-URL  http://efcl.info/adiary/038/tb/