僕でもわかる継続と部分継続

callcc と shift/reset についてわかるとこだけ書いてみます。

継続

callcc という操作は、現在から実行終了まで、継続をまるごと取り出します。例題。

p [1] + callcc {|k| [2] + k.call([3]) } #=> [1, 3]

callcc では callcc がリターンしてから実行終了するまでの継続 k が取り出せます。k.call([3]) で継続が呼ばれると、いきなり「callcc が [3] を返した瞬間」に実行が飛びます。つまりこんな感じ。

p [1] + [3]

あとは自明ですね。"[2] +" のあたりは無視されます。

部分継続

shift という操作は、現在から reset まで、継続の一部だけを取り出します。この継続の一部を部分継続といいます。例題。

p [1] + reset { [2] + shift {|k| [3] + k.call([4]) } } #=> [1, 3, 2, 4]

shift では shift がリターンしてから reset に到達するまでの部分継続 k が取り出せます。k.call([4]) で部分継続が呼ばれると、いきなり「shift が [4] を返した瞬間」に実行が飛びます。callcc と同じですね。

p [1] + reset { [2] + [4] }

そして reset のブロックは [2, 4] を返して終わります。すると今度はいきなり「k.call が [2, 4] を返した瞬間」に実行が飛びます。再開って感じ。

p [1] + reset { [2] + shift {|k| [3] + [2, 4] } }

shift のブロックは [3, 2, 4] を返して終わります。今度はまたいきなり「reset が [3, 2, 4] を返した瞬間」に実行が飛びます。飛びまくりですね。

p [1] + [3, 2, 4]

あとはそのまま。

ややこしい例

reset の中で複数回 shift するとややこしいですが、基本は同じです。

x = [1] + reset do
  shift {|k| [2] + k.call([3]) } +
  shift {|k| [4] + k.call([5]) } + [6]
end
p x #=> [1, 2, 4, 3, 5, 6]

k.call([3]) が呼ばれる。

x = [1] + reset do
  [3] +
  shift {|k| [4] + k.call([5]) } + [6]
end
p x

k.call([5]) が呼ばれる。

x = [1] + reset do
  [3] +
  [5] + [6]
end
p x

[3, 5, 6] が reset される。後に shift した方から順に再開していく感じ。ここでは k.call([5]) へ。

x = [1] + reset do
  shift {|k| [2] + k.call([3]) } +
  shift {|k| [4] + [3, 5, 6] } + [6]
end
p x

後半の shift のブロックが [4, 3, 5, 6] を返して終わるので reset に飛ぶけど、まだ shift が残っているのでそっちに飛ぶ。

x = [1] + reset do
  shift {|k| [2] + [4, 3, 5, 6] } +
  shift {|k| [4] + k.call([5]) } + [6]
end
p x

前半の shift のブロックが [2, 4, 3, 5, 6] を返して終わるので reset に飛ぶ。もう shift は残ってないのでそのまま reset が終わる。

x = [1] + [2, 4, 3, 5, 6]
p x

考察

なんだか飛びまくりでややこしいですが、要は部分継続の呼び出し k.call は「試しに shift をリターンさせて、reset に到達するまで実行して、その返り値を持ってくる」だけです。
また、上の最初の例では実は部分継続 k は proc {|arg| [2] + arg } と同じです。つまり、reset のブロックは 1 引数のクロージャであり、shift の呼び出しがその引数に置き換わっている、と見ることができます。


以上、Continuation Fest に行けなかったので、なんかぐだぐだ書いてみました。なんか間違ってたら教えてください。

おまけ

Ruby での shift/reset の実装。1.9 用。スレッドとかは考えない。

require "continuation"

def shift
  callcc {|c1| $ctn.(yield(proc {|v| callcc {|c2| $ctn = c2; c1.(v) } })) }
end

def reset
  callcc {|c| $ctn = c; v = yield; $ctn.(v) }
end