CPS是continuation passing style的縮寫,是一種編寫程式的風格,要理解這個概念,我們可以從為什麽要傳遞continuation開始。在循序式的語言(包含指令語言)裡面,continuation是明明白白寫出來的
- 要嘛是下一行
- 要嘛是jump到第 行
但有一種情況沒有寫,就是procedure。procedure從continuation的角度來看,是個一進一出的結構。在很多指令語言裡面都有calling convention,就是針對procedure的最佳化,因為這要求一進的時候復用現在的stack,輸入會放在特定的位置,一出的時候要把輸出寫到特定的registers
那可以多進多出嗎?可以,但我們先暫時轉向沒有明顯continuation的案例,並且討論函數式語言這種特例
在函數式語言裡面,允許傳遞lambda(匿名函數)以及其他各種高階運算式作為值,這其實給後端帶來了很大的複雜性,比如
let x := if a 1 2 in x
請問 if a 1 2 的continuation是什麼呢?是
let x := ♢ in x
♢ 用來表示任意運算式
那我們是不是可以把這寫成lambda?
\♢. let x := ♢ in x
這是合法的函數式寫法嗎?是。那我們可不可以說
let x := if a 1 2 in x
等於
let k := \♢. let x := ♢ in x in if a (k 1) (k 2)
答案還是可以。我們把這種把未來要執行的程式顯式提取出來的寫法,稱之為CPS
call/cc也就是這麼實現的。問題是,這樣傳遞的continuation通通都是同色的。比如你用call/cc做了個檔案資源清理,但呼叫的SQL連線程式丟了個exception(也用call/cc實現),這個檔案就不會被清掉了!這是資源洩漏,是非常嚴重的問題,所以scheme發展出 dynamic-wind 這個函數,用來保證裡面的運算不管發生什麼continuation跳轉都還是會執行
而且我們發現,傳統的continuation實作往往決定了一個任意的界線,例如頂層宣告以外的續延都不會被捕捉;而且我們又需要解決同色問題,所以我們發展了一些技術來處理這些
- delimited continuation:與其捕捉「所有剩下的計算(並設立任意的界線)」,不如先用prompt標出捕捉界線,abort會被prompt擋住
- 而不同用途的跳轉(exception、generator、async)可以用不同的prompt tag捕捉,他們就不會互相影響
- continuation mark這個功能則是可以把資料放到continuation frame上,之後用
current-marks沿著當下的continuation讀回來,並且保證tail call不會讓mark累積
當然,這些技術又細分成能不能多次abort,能不能多次resume等等,在runtime速度跟功能上做出取捨
最後CPS如果變成一種強制的形式,那就提供了一種優雅的編譯框架,因此這也是一種編譯理論技術。CPS變換強制把每個不明顯的control flow都打包成函數閉包,然後顯式的傳遞。做完之後有幾個性質會同時成立:每個中間值都有名字、每個呼叫都在tail position(所以call就是jump,return就是call傳進來的 k)、 化簡可以無條件使用,編譯器可以用這個IR解釋橫跨全局的控制流程,用來實現如exception等等高階控制流運算