読者です 読者をやめる 読者になる 読者になる

継続こわくない(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使おう

きちんと継続を理解するには!

@

@