LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

CPS系列 1:CPS(延續傳遞) 對正常人來說有什麼用途?

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) 可能會引入過多思考,請提前吃點核桃

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。