Continuation-Passing Style

Joseph Cheng
KKday Tech Blog
Published in
4 min readSep 15, 2018

Continuation-Passing Style 為一種程式撰寫的方式,其實每個人幾乎都使用過,最常見的例子是,我們在做非同步的時候所設計的方式

fun callApi(callback: (Response) -> Unit) {
Thread(Runnable {
val response = CallApi()
callback(response)
}).start()
}

在 call api 完取得結果之後,不是把結果放到 callApi function 的 return 值
反而是把結果傳遞到後面的 callback 裡面

一般來說,用這種方式的目的在於: lazy evaluation, async, control flow

舉個另一個例子來看,假設我們要設計一個 function sum,直觀的方式為

fun sum(a: Int, b: Int): Int {
return a + b
}
print(sum(1, 2)) // 3

使用 CPS 方式為

// 這邊我們不直接回傳結果,反而結果是回傳到 callback 身上
fun sum(a: Int, b: Int, callback: (Int) -> Unit) {
callback(a + b)
}
sum(1, 2) { print(it) } // 3

這邊我們程式的執行流程,不再是被一般一行一行的執行順序所控制,反而是由 callback 的呼叫來決定執行的順序
,所以我們常常在處理一些非同步 IO 的行為時,常常會使用這些撰寫方式;另外使用 CPS 的話 callback 一定是整個 function 處理完之後,才會把執行的結果放到 callback 裡面去,所以 callback 是用 lazy 的方式去執行的

用這種方式也可以有多種的變換

fun square(a: Int, callback: (Int) -> Unit) {
callback(a * a)
}
fun sum(a: Int, b: Int, callback: (Int) -> Unit) {
callback(a + b)
}
sum(1, 2) { s -> square(s) { print(it) } } // (1 + 2)^2 = 9

不過這種方式的可讀性較差,會造成 callback hell,也才會出現像 Javascript promise 或者 rx系列來解決 callback hell 的問題

也可以調整一下 parameter 的順序,讓程式的可讀性好一些

fun square(callback: (Int) -> Unit) = fun(a: Int) {
callback(a * a)
}
fun sum(callback: (Int) -> Unit) = fun(a: Int, b: Int) {
callback(a + b)
}
sum(square { print(it) })(1, 2) // (1 + 2)^2 = 9

學術一點來看,為什麼叫 Continuation-Passing 呢,原因是正確的來說 callback 應該是被稱作為 continuation,指的是當 function 執行取得結果之後,應該”繼續”的部份

這種方式也滿常用在 recursion 身上

fun sum(nums: List<Int>, cont: (Int) -> Unit) {
val first = nums.first()
val remaining = nums.drop(1)
if (remaining.isEmpty()) cont(first)
else sum(remaining) { cont(first + it) }
}
sum(listOf(1, 2, 3, 4)) { print(it) } // 10

以上面這個例子來看,實際執行的過程為

cont(1 + cont(2 + cont(3 + cont(4))))

這邊要反著看,第一次 sum 實際結束是在 4 的時候,因為

if (remaining.isEmpty()) cont(first)`

所以就 cont(4),接著

else sum(remaining) { cont(first + it) }

所以 cont(3 + cont(4)),一直執行下去,最後才會變成

cont(1 + cont(2 + cont(3 + cont(4))))

--

--