CPS(Continuation Passing Style)編程是一種編程風格,它的主要思想是將程式的控制流程顯示地傳遞給下一個函式,而不是通過函式呼叫堆疊來控制。
在 CPS 編程中,每個函式都需要一個額外的參數,這個參數被稱為 "continuation" (或者我們一般叫他 k
),它是一個函式,表示程式執行完當前函式後要繼續執行的程式碼。
在函式執行完之後,它會將結果傳遞給 continuation 函式,從而控制程式的執行流程。這樣,函式呼叫就變成了一個連續的函式呼叫鏈,每個函式都負責將結果傳遞給下一個函式,從而實現了控制流的顯示傳遞。
我和主流作者不同的是我會用正常人看的編程語言來講解 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));
}
假如有個邏輯改變,你只需要改變一個函式就可以了,而不是整個函式
_whatever3(add12, add34);
_whatever3((k)=>k(0), add34);
// 思考一下下面的發生了什麼?
_whatever3((k)=>0, add34);
很簡單,k
作為這個函式的延續,假如你並沒有呼叫 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) 可能會引入過多思考,請提前吃點核桃