トップ «前の日記(2007-06-15) 最新 次の日記(2007-06-21)» 編集

Route 477



2007-06-20

[ruby][event] Ruby勉強会@関西-16「30分でわかるcallccの使い方」

先週末のRuby勉強会@関西で、Rubyにおけるcallccの使い方について発表させていただきました。

継続の説明については「なんでも継続」がよく参照されるんだけど、 ちょっと説明がボトムアップすぎると思うので(僕も最初に読んだときは全然分からなかった)、「callccで何ができるか」という応用面から攻める 構成にしてみました。

最初は「継続かわいいよ継続」「それをすてるなんてとんでもない」と思ってたんだけど、 いろいろ調べてるうちになんでcallccが嫌われるのかが理解できてしまった。callccはかわいいけど、非常に手のかかる奴らしい。 しかも、面白い利用例はいっぱいあるけど実用的な例があんまりないんだよね^^;。

callccが無くなるとRubyの「かっこよさ」が1さがる。そのかわりに「がんじょうさ」が1あがる。さあ、あなたはどちらを望みますか?

以下テキスト版。

要旨

  • callccって何?
    • RPGのセーブ・ロードみたいに、プログラム中でセーブポイントを作って「そこからやり直し」したりできるものだよ
  • callccは凄い!
    • 利用例をいくつか紹介します
  • callccは危ない!
    • プログラムを変なところから「再開」できてしまうので、Ruby本体や拡張ライブラリの実装が面倒になるよ
    • Ruby 1.9では、callccの機能の一部を実現する「Fiber」という機能がテストされてるよ

callccって何?

  • キーワード:継続、Continuation、callcc
  • ドラクエ的に言うと
    • 王様と話すとセーブできる
    • ドラゴンのこうげき!
    • 89のダメージ
    • まつもとはしんでしまった
    • 王様「まつもとよ、しんでしまうとはふがいない」
    • →セーブしたところからやりなおし
  • callccはセーブポイントに似ている
    • callccでセーブ、cc.callでロード
  • callcc{}の返り値:
    • cc.call(arg)で飛んできたときはarg
    • そうでない時(最初の一回)はブロックの返り値
  • まとめ
    • セーブ = callcc{|cc| … } # ccがセーブポイント
    • ロード = cc.call
    • callccの「次の処理」から再開される

callccは凄い!

(1) 3重ループを一発で脱出
callcc{|cc|
  for i in (0..10)
    for j in (0..10)
      for k in (0..10)
        if i==j && j==k
          cc.call
        end
      end
    end
  end
}

それcatchでできるよ

catch(:escape){
  for i in (0..10)
    for j in (0..10)
      for k in (0..10)
        if i==j && j==k
          throw :escape
        end
      end
    end
  end
}

google code searchではループ脱出にcallccを使ってる例が 結構あったけど、throw/catchでできるよ

(2) メソッドを「少しずつ」実行する

ゲームのイベント処理

def event
  king.say(“おお#{@name}よ、しんでしまうとはふがいない”)
  wait_ok
  king.say(“そなたにもういちどきかいをあたえよう”)
  wait_ok
  king.say(“では ゆけ #{@name}よ!”)
end

wait_okをどのように実装すればいい?

def wait_ok
  loop do 
    input.poll
    break if input[”ok”]  #これは他の処理が
    sleep 1                 #止まってしまうのでダメ
  end
end

普通に書くと非常にめんどう(´・ω・`)

def event(input)
  case @step
  when 0
     king.say “おお#{@name}! しんでしまうとはふがいない”
     @step += 1
  when 1
     @step += 1 if input[“ok”]
  when 2
     king.say “そなたに もういちど きかいを あたえよう”
     @step += 1
  when 3
     @step += 1 if input[“ok”]
  when 4
     king.say “では ゆけ #{@name}よ!”
  end
end

callccを使うと…

def event
  king.say(“おお#{@name}よ、しんでしまうとはふがいない”)
  wait_ok
  king.say(“そなたにもういちどきかいをあたえよう”)
  wait_ok
  king.say(“では ゆけ #{@name}よ!”)
end
  • メソッドを「少しずつ」実行できる
  • wait_okが呼ばれたたら、セーブしてreturn
  • 次回の呼び出しはセーブしたところから再開
(3) 全探索を簡単に
  • あるマンションに5人の男が住んでいる
    • bakerは5Fではない
    • cooperは1Fではない
    • millerはcooperより上の階にいる
    • smithとfletcherは1つ隣の階にいる…
require "amb"

A = Amb.new

baker = A.choose(1, 2, 3, 4, 5)
cooper = A.choose(1, 2, 3, 4, 5)
fletcher = A.choose(1, 2, 3, 4, 5)
miller = A.choose(1, 2, 3, 4, 5)
smith = A.choose(1, 2, 3, 4, 5)

A.assert([baker, cooper, fletcher, miller, smith].uniq.length == 5)
A.assert(baker != 5)
A.assert(cooper != 1)
A.assert(fletcher != 1 && fletcher != 5)
A.assert(miller > cooper)
A.assert((smith - fletcher).abs != 1)
A.assert((fletcher - cooper).abs != 1)

p [baker, cooper, fletcher, miller, smith]
(4) eachを「少しずつ」回す
  • C++風のイテレータ
  • requrie ‘generator’ (標準添付)
g = Generator.new([1,2,3])
while g.next?
  p g.next
end
(5) ppp
irb(main):109:0> a = 1
1
irb(main):112:0> ppp :a
a = 1
  • 実装は超複雑
  • Ruby界の3大黒魔術が夢の競演!
    • eval系 (eval, *_eval)
    • フック系 (*_missing, set_trace_func)
    • callcc

callccは危ない!

  • なぜ危ない?
    • (Rubyの) バグの原因になりやすい
    • 油断するとRubyが落ちる
  • 例:fooというブロックを取るメソッドがCで実装されていたとする。
foo do
  puts "hoge"
  puts "moge"
end
static VALUE rb_foo(VALUE ary1)
{
   int *p = malloc(...);
   …
   rb_yield();
   …
   free(*p);
}

ブロック内でcallccを使うと、rb_yield(); 以下が2回実行されてしまい、メモリを2重開放しようとしてRubyが落ちる。

これを避けるには、Ruby開発陣や拡張ライブラリの作者が常にcallccの存在を気にしてコーディングしなければならない。

まとめ

callccを使うと
スパゲッティコードが簡単に書ける!
  • callccはとても強力
    • 処理の流れを自在に操れる
  • でも他人に読めないコードになりやすい
    • 見えないところに隠そう
  • callccは楽しい
    • 思いもよらないことができる
  • でもRubyインタプリタのバグ要因になり易い
    • 将来無くなるかも

おまけ(Fiberについて)

callccの代表的な使い方は

  • (A) 処理の中断/再開 (generator, wait_ok)
  • (B) 処理のやり直し (amb, ppp)

の2通りが挙げられる。

callccが危険なのは(B)ができてしまうからだ。 じゃあ(A)の機能だけなら残してもいいかも?ということで、Ruby 1.9ではFiberという機能が検討されている。

FiberはThreadみたいなものだけど、Threadのように自動的に並列実行はされず、Thread#yieldで明示的に移動先を切り替えてやる。 軽量スレッドとも呼ばれるらしい。(Thread=糸、Fiber=繊維)

Fiberを使うと、callccの機能のうち(A)を簡単に実現できる。

Ruby1.9は継続と“Fiber”をサポート - @IT …と書かれてるけど、実際はまだ決まってない。あり得る選択肢は:

  1. Fiberだけが残る
  2. callccもFiberも残る
  3. callccもFiberも残らない
  4. callccだけが残る

(↑一応、ありそうな順に並べてみた)

  • 今後に注目
    • 1.9.xは今年のクリスマスにリリース予定です