CPS(Continuation Passing Style)プログラミングは、プログラムの制御フローを次の関数に明示的に渡すことによって、関数呼び出しスタックではなく制御フローを制御するという主要なアイデアに基づいたプログラミングスタイルです。
CPS プログラミングでは、各関数には追加のパラメータが必要であり、これは「継続」と呼ばれます(または一般的に k
と呼ばれます)。これは、現在の関数の実行が完了した後に続けて実行するコードを表す関数です。
関数の実行が完了すると、結果は継続関数に渡され、それによってプログラムの実行フローが制御されます。これにより、関数呼び出しは連続した関数呼び出しチェーンになり、各関数は結果を次の関数に渡す責任を持ち、制御フローの明示的な渡しを実現します。
私と一般的な著者の違いは、私が CPS を説明するために一般的なプログラミング言語を使用することであり、Scheme
のような奇妙な言語を使用しないことです。
// 通常の関数
function add(a, b) {
return a + b;
}
// CPS関数
function _add(a, b, k) {
return k(a + b);
}
誰かがこれらには顕著な違いがあるのか尋ねるかもしれません。
あります!しかも大きな違いがあります。例を挙げましょう。
function whatever() {
const a = add(1, 2);
const b = add(3, 4);
return a + b;
}
普通の人がこのコードを書くのは非常に明白です。
function _whatever() {
return _add(1, 2,
(a) => _add(3, 4,
(b) =>
a + b));
}
このようにコードを書くことにはどのような利点がありますか?
率直に言って、もし私があなたの上司であれば、このようにコードを書いた場合、最初にあなたをクビにします。
ただし、このように書くと見栄えが良くなります。
function _whatever2() {
// 手動でカリー化する
const call = (f) => (...args) => (k) => f(...args, k);
const add12 = call(_add)(1, 2);
const add34 = call(_add)(3, 4);
return add12((a) => add34((b) => a + b));
}
このようにすると、連続した関数を個々の関数に分割できるため、どこでもこの関数を呼び出すことができます。関数の最後だけでなく、どこでも呼び出すことができます。
これを行うことの利点は、関数の制御フローを任意に渡すことができることです。関数の最後だけでなく、複数の場所で関数を呼び出すことができます。そのため、この関数を複数の関数に分割することで、関数の再利用が可能になります。
const call = (f) => (...args) => (k) => f(...args, k);
const add12 = call(_add)(1, 2);
const add34 = call(_add)(3, 4);
function _whatever3(add12, add34) {
return add12((a) => add34((b) => a + b));
}
もしロジックを変更する必要がある場合、関数全体を変更するのではなく、1 つの関数だけを変更すれば済みます。
_whatever3(add12, add34);
_whatever3((k)=>k(0), add34);
// 以下の結果は考えてみてください
_whatever3((k)=>0, add34);
非常に簡単です。この関数の継続として、k
を呼び出さないまま値を返すと、その値が結果として返されます。
これにより、より多くの組み合わせが可能になります。
では、このものは何に使うのですか?私は以前のコードを書いていたのに、なぜこれを学ぶ必要があるのですか?#
例えば、エラーハンドリングの例がこの問題を非常によく説明しています。
// エフェクトを手動で調整して効果を確認してください
var fail = false;
function maybe_error_function_(ok_do, error_do, k) {
if (!fail) {
ok_do(k);
} else {
error_do(()=>{});
}
}
function main() {
const ok_do = (k) => {
console.log("ok");
k();
};
const error_do = (k) => {
console.log("error");
k();
};
const before = call(_add)(1, 2);
const maybe_err = call(maybe_error_function_)(ok_do, error_do);
const after = call(_add)(3, 4);
return before((befor_result) => maybe_err((maybe_err_result) => after((after_result) => {
console.log(befor_result + after_result);
console.log("done");
})));
}
気づくでしょう、おっと!なんてことだ、エラーハンドリングのロジックが通常のロジックと同じように書かれている???
私は今あなたに言えます、これは Rust 言語の ?
演算子の実装原理です。
これは、コンパイラレベルでこの関数を個々の関数に分割し、これらの関数を組み合わせ、そしてインライン化することで、?
演算子の機能を実現しています。
次の章では、「限定継続(Delimited Continuation)」とは何かについて説明しますが、考えることが多くなるかもしれませんので、事前にくるみを食べてください。