さっさと続き書けよって感じだけど。また横道へ。
継続渡し形式で手続きは、概念的に「呼び出し元へ戻る」ってことがなくなるので、例えばC#でちゃんとした継続渡し形式の手続きを書こうとすると、複数の文(セミコロンが2個以上)になることはないような気がしたのでメモメモ。
Foo(); Bar();
は、「Fooの呼び出しから戻ってきた後で、Barを呼び出す」という意図があると思われるが、Fooからは戻ってこないので
FooCps(()=>{ Bar(); cont(); });
みたいな感じに書ける。FooCpsは継続渡しになったけどBarが継続渡しになっていないので、Barに継続を渡すための引数を追加して
FooCps(()=>BarCps(cont)); // contは継続
こんな感じになるか。
Fooの実行がBarからも可視となるいかなるオブジェクトも変更しない(例えば、FooとBarがクラスCのメソッドでありFooの実行によってCのどのフィールドも変化しない)とすると、Fooの実行はBarの実行に影響を与えない、つまりFooの継続(すなわちBar)に必要な情報がないことになるので、上のように書ける。
しかし、ここで
result = Foo(); Bar(result);
のように、Barの実行のために必要な情報が、それ以前の計算結果で得られる場合は
Foo(result=>Bar(result, cont)); // contは継続
みたいになる。
以上を踏まえると、継続渡しは
ような気がする。
前者に関しては、最近は手続きを値として扱える言語は珍しくないので、特に問題は無いか。
後者に関しては、概念的には手続きから戻ってこないけど、C#など多くの言語の実装(仕様?)では、スタックを利用した「戻り番地へ戻ってくる」ことでプログラムの流れが制御されることが原因か。一方でSchemeは「手続き呼び出しは継続を伴う引数付きgoto」ってことが言語仕様として規定されているので、少なくとも戻り番地の情報が不要になるんだろう。このことがスタックオーバーフローを起こさない(?)ことの理由なのかな?
Scheme以外の関数型言語ではどうなんだろう。関数型言語は関数(手続き)が一級オブジェクトになるので、呼び出しの深さが深いからといってスタックオーバーフローしてちゃ使いモンにならんよなぁ。
継続渡し形式とCall-Return方式*1って積極的に混在させるもんなのかなぁ。何となく相性が悪い気がする。
とくに結論とかは無いよ。脳内だだ漏れ。
*1:継続渡し形式ではなく、「手続きは呼び出し元に戻ってくる」という考えに基づく手続きの呼び出し。「プログラミングGacuhe」のP277より。