[717] Re:イテレータ(とCPS)
    
    投稿者:(ぱ)
    2007/02/20 02:13:25
    
    
    土曜はとあるイベントにて大阪で飲んだくれておりまして、ずいぶん返事が遅くなりまして
すみません。
それプラス、私自身、クロージャに慣れておらず継続もわかっておらず、今回のNykRさんの
サンプルはずいぶん荷が重いものでした。とはいえ放り出していては勉強にならんので、
私なりに解釈してみました。間違い等ありましたらご指摘ください。
さて、単に木をイテレートするだけなら、
tree.each(closure(item) {
  # このitemについてなんかする
});
という形のeachメソッドをツリーに装備するのは簡単で、ツリーの中で再帰して、
要素ごとに、渡されたクロージャを呼び出せばよいのですが、こういう形式(内部イテレータ)
だと、あるツリーについて、「ここまで走査した」という状態を保存しておけないのが
問題になるわけですよね。その点、NykRさんの提示されたイテレータでは、こういう
書き方も許される。
# ふたつのイテレータでツリーを並列に走査する。
for (ite1 = iterator_GoF(foreachTree_CPS(tr)),
       ite2 = iterator_GoF(foreachTree_CPS(tr));
     !ite1.isDone() && !ite2.isDone();
     ite1.next(), ite2.next()) { 
    print(" " + ite1.currentItem()); 
    print(" " + ite2.currentItem()); 
} 
これができるのが外部イテレータの強みです。
Cなどで、再帰を使ってツリーを辿る場合、「ここまで走査した」という情報がスタック上に
ありますから、スタックが1本しかない以上、ふたつのイテレータを並行して使うことは
できないわけです(片方が片方の繰り返しに完全に含まれるなら可能)。
tr = {13,{5,{3,{},{}},{13,{12,{},{}},{}}},{16,{16,{15,{},{}},{}},{17,{},{}}}}; 
この例では、nodeの[0]にそのノードの値が、[1]に左の子が、[2]に右の子が
入っていますから、再帰で間順で走査するなら以下のようになります。
function internalIterate(node) {
  if (node.size() == 0) {
  } else {
    internalIterate(node[1]); # ...(a)
    print(" " + node[0]); # とりあえず表示しておく
    internalIterate(node[2]);
  }
}
(a)の箇所で左の子について再帰呼び出しを行い、それが戻ってきてから続きの処理を
やっています。「戻ってきてから続きの処理をやる」ことができるのが再帰の嬉しい
ところですが、それを期待していては、外部イテレータは作れない。
そこで、「戻ってきてから続きの処理をする」のではなく、「戻ってきてから
やってほしい続きの処理をクロージャとして渡す」とすれば、関数から戻ってこなくても
よくなる。その変換をしたのがこれ。
function internalIterate(node, cont) { 
  if (node.size() == 0) { 
    cont();
  } else { 
    internalIterate(node[1], 
		    closure () { # 続きをクロージャで渡す。
                      print(" " + node[0]);
		      internalIterate(node[2], cont); 
		    }); 
  }
}
# 第2引数には「処理の続きを渡す」のだから、最後に「おしまい」と表示される。
internalIterate(tr, closure() {print("おしまい");});
さて、この状態では、要素ごとに行う処理がprint()に固定されていますから
汎用性がありません。では引数でなにか関数を渡せば、とこうしても、
    internalIterate(node[1], 
		    closure () { # 続きをクロージャで渡す。
                      proc(node[0]); # 処理を引数procで渡す
		      internalIterate(node[2], cont); 
		    }); 
これでは内部イテレータと同じですから意味がありません。
そこで、このproc()にも、続きの処理をクロージャで渡してやることにします。
function foreachTree_CPS(proc) {
  return closure internalIterate(node, cont) { 
    if (node.size() == 0) { 
      cont();
    } else { 
      internalIterate(node[1], 
                      closure () { 
                        proc(node[0],
                             closure() {
                               internalIterate(node[2], cont);
			     });
                      }); 
    }
  };
}
外部イテレータの場合、このprocの第2引数をnext()の際に呼び出せばよいわけですね。
なぜならそれが「処理の続き」だから。
イテレータはこうなります。
function iterator_GoF(foreach_CPS, collection) { 
  this = new_object(); 
  this.first = closure () { 
    this.isDone = closure () { return false; }; 
    foreach_CPS(closure (item, cont) { 
                  this.currentItem = closure () { return item; }; 
                  this.next = closure () { cont(); }; 
                })(collection, closure(){ # ..(b)
                                 this.isDone = closure () { return true; }; 
                               });
    }; 
    this.first(); 
    return this; 
} 
イテレータのfirst()が呼び出されたときに、foreach_CPS()を呼び出して、
internalIetrate()を取得し、それに対しcollectionとクロージャを渡しています(b)。
これがつまり元のソースの
internalIterate(tr, closure() {print("おしまい");});
に相当するわけですが、「おしまい」と表示する代わりに、isDoneについて、
trueを返すよう関数を差し替えています。
んで、internalIterate()に渡しているproc()では、
    this.currentItem = closure () { return item; }; 
    this.next = closure () { cont(); }; 
currentItemを設定後、nextに対し、contすなわち「処理の続き」をセットしています。
これで、次に利用者がnext()を呼び出したとき、internalIteratorの続きが実行される、と。
NykRさんのソースとは微妙に変わっていますが、こんな解釈でよいでしょうか?
# いやあ、実に勉強になりました。