継続渡しに関する無駄思案

さっさと続き書けよって感じだけど。また横道へ。
継続渡し形式で手続きは、概念的に「呼び出し元へ戻る」ってことがなくなるので、例えば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#とかJavaとかだとコールスタックをもりもり消費してしまう

ような気がする。
前者に関しては、最近は手続きを値として扱える言語は珍しくないので、特に問題は無いか。
後者に関しては、概念的には手続きから戻ってこないけど、C#など多くの言語の実装(仕様?)では、スタックを利用した「戻り番地へ戻ってくる」ことでプログラムの流れが制御されることが原因か。一方でSchemeは「手続き呼び出しは継続を伴う引数付きgoto」ってことが言語仕様として規定されているので、少なくとも戻り番地の情報が不要になるんだろう。このことがスタックオーバーフローを起こさない(?)ことの理由なのかな?
Scheme以外の関数型言語ではどうなんだろう。関数型言語は関数(手続き)が一級オブジェクトになるので、呼び出しの深さが深いからといってスタックオーバーフローしてちゃ使いモンにならんよなぁ。

継続渡し形式とCall-Return方式*1って積極的に混在させるもんなのかなぁ。何となく相性が悪い気がする。


とくに結論とかは無いよ。脳内だだ漏れ。

*1:継続渡し形式ではなく、「手続きは呼び出し元に戻ってくる」という考えに基づく手続きの呼び出し。「プログラミングGacuhe」のP277より。