K.Maebashi's BBS

ご自由に書き込んでください。雑談も可。
テスト書き込みの類はテスト用掲示板にどうぞ

[日付順表示] [日付順インデックス] [スレッド順インデックス]

新規投稿 | 開設者ホームページへ戻る | ヘルプ

[695] イテレータ(とCPS)
投稿者:NykR
2007/02/20 02:13:25

http://kmaebashi.com/programmer/devlang/regexp.html > Java流の、要素を取り出すだけで強制的にポインタを進めてしまう仕様は、 > ちょっとどうかと思うんですがねえ。 同感です。マージとかやりにくそう。 というわけでどちらか片方にするなら、GoFの方が良いな、と思ってたりするのですが、以下のような関数を用意すれば、GoF方式とJava方式の両方をあまり手間をかけずに作ることができます。(両方欲しい、という訳ではありませんが) function iterator_GoF(foreach_CPS) { 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(null); }; }, closure (ret) { this.isDone = closure () { return true; }; }); }; this.first(); return this; } function iterator_Java(foreach_CPS) { this = new_object(); closure () { this.hasNext = closure () { return true; }; foreach_CPS(closure (item, cont) { this.next = closure () { cont(null); return item; }; }, closure (ret) { this.hasNext = closure () { return false; }; }); }(); return this; } function foreach(foreach_CPS, proc) { # ついでに作った foreach_CPS(closure (item, cont) { cont(proc(item)); }, closure (ret) {}); } # foreach_CPSは CPS(continuation passing style, 継続渡し形式)で書かれたforeachです。 # つhttp://www.shiro.dreamhost.com/scheme/docs/cont-j.html これらの関数に対し、コレクション側でforeach_CPSを用意して渡せば それぞれの関数に対応したイテレータが返されます。 例えば以下のような2分木があったとすれば tr = {13,{5,{3,{},{}},{13,{12,{},{}},{}}},{16,{16,{15,{},{}},{}},{17,{},{}}}}; function foreachTree_CPS(tree) { return closure (proc, fin) { closure internalIterate(node, cont) { if (node.size() == 0) { cont(null); } else { internalIterate(node[1], closure (ret) { proc(node[0], closure (ret) { internalIterate(node[2], cont); }); }); } }(tree, fin); }; } という関数を1つ定義するだけで print("foreach:"); foreach(foreachTree_CPS(tr), closure (item) { print(" " + item); }); print("\n"); print("GoF :"); for (i = iterator_GoF(foreachTree_CPS(tr)); !i.isDone(); i.next()) { print(" " + i.currentItem()); } print("\n"); print("Java :"); for (i = iterator_Java(foreachTree_CPS(tr)); i.hasNext();) { print(" " + i.next()); } print("\n"); のように、3種類のイテレータが使えてまあ便利。 でもシーケンシャルなコレクションだとそれほど嬉しくはありません。 CPSではforやwhileがまともに使えないので、例えば配列用のforeach_CPSは function foreachArray_CPS(array) { return closure (proc, fin) { closure loop(index, cont) { if (index == array.size()) { cont(null); } else { proc(array[index], closure (ret) { loop(index + 1, cont); }); } }(0, fin); }; } なんてことになって、要素数が多いとforeachに渡したときにクラッシュするなあ、とか、初期値と変数が遠いなあ、とか。 今の実装だと末尾再帰の最適化は物凄く大変そうですしね。 # でもこの形式のループは個人的にはむしろ欲しかったりします、crowbar ver.0.3.02にcall/ccを付けたので。ちなみに、この時点で末尾再帰の最適化はやりやすくなったのですが、CRB_Objectが増えたので先にGCを改造しなければならないのでした(どうでもいい報告)。 この手法は、 ・外部イテレータを直接作るのが難しくて ・ループによらないアクセスが簡単な データ構造(木とか) *では* 役に立ちます。 # ライブラリが簡単に書けます。というのはそれほど嬉しいことではない気がする
[この投稿を含むスレッドを表示] [この投稿を削除]
[702] crowbar ver.0.4のバグを修正しました
投稿者:(ぱ)
2007/02/20 02:13:25

># つhttp://www.shiro.dreamhost.com/scheme/docs/cont-j.html 紹介していただいたshiroさんのページは、以前読んだことはあったのですが、 あまり理解できずにそのままになっていました。 そこで今回、以下のページのActionScript版をベースに、crowbar版を書いて 試してみたところ… クラッシュしました。 http://torus.jp/memo/x200403/nandemo_keizoku_as.rd.html 調べてみたら、「配列リテラルの要素にオブジェクトを格納すると、一時的に スタックからの参照が切れることがあり、そのタイミングでGCすると死ぬ」という 潜在バグが(おそらくver.0.2から)あり、かつ、ver.0.4では「毎回GCする」という テスト用の状態でリリースしてしまったために発覚しました。 取り急ぎ修正版をリリースしました。毎度テストが甘くすみません。 力尽きたので今日はここまでです。
[この投稿を含むスレッドを表示] [この投稿を削除]
[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さんのソースとは微妙に変わっていますが、こんな解釈でよいでしょうか? # いやあ、実に勉強になりました。
[この投稿を含むスレッドを表示] [この投稿を削除]