継続こわくない(RubyでFiberを使ったコードをcallccで書きなおしてみた)
Fiberに関するこんな記事をみて、
そういえば以前30分でわかるcallccの使い方で、
callccの代表的な使い方は
* (A) 処理の中断/再開 (generator, wait_ok)
* (B) 処理のやり直し (amb, ppp)
の2通りが挙げられる。
callccが危険なのは(B)ができてしまうからだ。じゃあ(A)の機能だけなら残してもいいかも?ということで、Ruby 1.9ではFiberという機能が検討されている。
と書いてあったのを思い出して、「Fiberはcallccの抽象化ってことか……じゃあFiberのコードはcallccで書き直せるのかな?」
と思い試してみました。
Fiberのコード
はこべにっき#より、改変
require 'fiber' def count() n = 0 Fiber.new do loop do Fiber.yield n n += 1 end end end c = count 10.times do p c.resume end # 実行結果 # 0 # 1 # 2 # 3 # 4 # 5 # 6 # 7 # 8 # 9
Fiberについて
なんでこうなるかちっともわかりません>< まったく未知の概念にであった気分でした。
おぼろげな理解によるとどうもFiberは中断・再開ができるもので、
Fiber.new do 〜 end の中でFiber.yieldするとそこが中断・再開ポイントになるみたいです。
なおFiber.new do 〜 endは全く本質的ではないみたいでPythonならば
def count(): n = 0 while 1: yield n n += 1
このように書けます。こっちのほうがだいぶわかりやすいですね。
callcc(の20%くらいの機能)について
callccはすごく強力すぎて使いづらいgotoみたいな物です。
ただしgotoのラベルみたいな箇所の直前の状態(ローカル変数の定義、スタックフレーム)を黄泉がえらせるという特性を持ちます。
gotoのラベル: callcc{|Continuation| (ほげほげ)}
goto: Continuation#call
callccの返り値はContinuation#callから呼ばれたときはその引数になります。
a = callcc{|c| @c = c} p a #=> nil (1回目) #=> :hoge (2回目(@c.call(:hoge))されたとき) ... @c.call(:hoge)
無限ループを書いてみるとこんな感じ
変数の定義は保存されますが変数の値が変わっていた場合そのままです。
require 'continuation' #ruby1.9の時だけ必要 n = 0 callcc{|c| @c = c} # @c.callが呼ばれるとココ(callcc{}が終了(返り値を返した))した状態を黄泉がえらせる p n += 1 @c.call
あんまりかっこ良くない文法ですね
callccのコード
Fiberのコードをcallccで書き直すとこうなります。(このサイトを参考にしました)
require 'continuation' class Count def initialize @resume = proc do |ret| n = 0 loop do n += 1 ret = callcc do |cont| @resume = cont ret.call(n) end end end end def resume callcc do |ret| @resume.call(ret) end end end c = Count.new 10.times do p c.resume end
邪悪すぎる。
実行順序
1回目
@resume = proc do |ret| # 1 Re黄泉がえり,return先を受け取る n = 0 # 2 loop do # 3 n += 1 # 4 ret = \ # 1回目は代入される前に復帰する callcc do |cont| # 5 @resume = cont # 6 次に呼ばれたときにはcallccから再開するように ret.call(n) # 7 呼び出したときの状態へのRe黄泉がえり 処理中断 end end end
2回目以降
@resume = proc do |ret| n = 0 loop do # 3 n += 1 # 4 ret = \ # 1 新しいRe黄泉がえり,return先を受け取って処理再開 callcc do |cont| # 5 @resume = cont # 6 ret.call(n) # 7 呼び出したときの状態へのRe黄泉がえり 処理中断 end # 2 end end
このコードではcallccが2回も使われています。
これは「行き」と「帰り」で2回callが必要になるからです。
それから最初はret = callcc do ... のret=の必要性が分からず苦しみましたが、
これは帰還先であり、当然帰還先は毎回変わるので、新しいところに帰らないと無限ループしてしまう訳です。
もしret=がないと……
c = Count.new c.resume # 1回目 呼び出し↓ # 1回目帰還先 & 2回目帰還先(!?) c.resume # 2回目 呼び出し↑
ステキな無限ループですね!
感想
Callccむずかしい。あたまはれつしそう
Fiberかんたん
Fiber使えるときはFiber使おう
きちんと継続を理解するには!
やっぱ継続むずかしい。あたまはれつしそうです
2011-02-25 23:20:08 via web
@_ko1
@yayugu 実装してみればすぐわかるよ
2011-02-25 23:25:25 via Tween to @yayugu