継続とは
途中までの結果を受け取って先へ進む、残りの計算を継続(continuation)と呼びます。たとえば 1 + (2 * 3) で 2 * 3 を計算しているとき、残っているのはその結果に 1 を足す計算です。この継続は v => 1 + v と書けます。
途中までの結果を受け取って先へ進む、残りの計算を継続(continuation)と呼びます。たとえば 1 + (2 * 3) で 2 * 3 を計算しているとき、残っているのはその結果に 1 を足す計算です。この継続は v => 1 + v と書けます。
reset と shiftreset は、継続をどこまで取り出すかの境界(delimiter)を作ります。その内側で shift を呼ぶと、最も内側の reset までの残りの計算が、関数 k としてハンドラーに渡ります。ハンドラーが k を呼ばずに値を返すと、残りの計算は打ち切られます。
Scheme などの call/cc は、プログラムの末尾までの継続を取り出します。その継続を呼ぶと、呼び出し元の残りの計算を飛ばします。shift で取り出した継続の範囲は reset までです。呼び出し元に値が返るので、関数のように組み合わせて使えます。この仕組みで call/cc も再現できます。
shift より前に作った変数やオブジェクトへの変更は残ります。async やジェネレーターをまたぐ捕捉や、再開しなかった継続を finally で片付ける処理は対象外です。shift と reset の動きを式の評価順に追えます。サンプル名を選ぶと、実行画面でそのコードが開きます。
shift は、最も内側の reset までの残りの計算(ここでは 1 + [ ])を関数 k として取り出します。ハンドラーから k(2) を呼ぶと、1 + 2 の結果 3 がハンドラーに返ります。これを 10 倍した 30 が reset の結果です。
shift が捕捉するのは、最も内側の reset までの計算です。内側の reset があると、10 を足す計算だけが打ち切られます。外側は戻り値の 7 に 100 を足し、107 を返します。内側の reset を外した場合、結果は 7 です。
shift のハンドラーが継続 k を呼ばずに 42 を返すと、reset 内の残りの計算は打ち切られます。reset の結果も 42 です。
ログの番号 1〜6 を追うと、計算がどこで中断し、どこへ戻るかがわかります。shift でハンドラーに移り、k(2) で中断箇所から再開します。reset の末尾まで進むと、結果の 3 がハンドラーに返ります。
shift(k => k) は、継続 k を reset の外へ返します。変数に保存しておけば、後から別の値で再開できます。このデモは multi-shot モードなので、同じ継続を何度でも呼べます。
一つのハンドラーで k(2) と k(3) を呼ぶと、残りの計算 1 + [ ] がそれぞれの値で実行されます。結果の 3 と 4 を足して 7 を返します。
multi-shot は再開のたびに制御状態を複製します。変数やヒープの状態は巻き戻りません。shift の前に作った count はすべての再開で共有されるので、呼ぶたびに 1, 2, 3 と増えます。
再開した計算から、もう一度 shift を呼べます。first(2) は最初の値を 2 に固定した継続 a を返し、first(5) は 5 に固定した継続 b を返します。a と b はそれぞれ独立して再開できます。
choose は配列の要素を一つずつ k(item) に渡します。x と y の両方を choose で選ぶと、ループを入れ子にせず全通りの組み合わせを試せます。
choose で候補を試し、条件に合わない枝は guard で打ち切ります。guard 内の shift は k を呼びません。共有配列への変更は巻き戻らないため、枝ごとに columns.concat([column]) で新しい配列を作ります。
二分木を再帰でたどり、葉を見つけるたびに値と継続を返します。両方の木から葉を1枚ずつ取り出して比べます。2回目の比較では 2 と 9 が異なるため、最後の葉 3 を読む前に止まります。
flip(p) は、表と裏の両方で同じ継続を再開します。それぞれの結果に起こる確率を掛け、足し合わせます。表の確率が 0.25 と 0.75 の独立なコインを使い、表がちょうど1回出る確率と、その条件で最初のコインが表である確率を求めます。
外側の reset を境界にして、Scheme 風の call/cc を shift で再現します。callCC が渡した関数を呼ぶと、shift(ignored => k(value)) が呼び出し側の残りの計算を捨て、callCC の位置へ戻ります。
perform は操作名、引数、再開用の継続 resume を要求オブジェクトにまとめます。handle は要求に応じて状態を更新したりログを記録したりし、step.resume(response) で計算を再開します。program には計算を書き、副作用の実装は handle に置きます。