## Continuation
continuation對于編程,就像是達芬奇密碼對于人類歷史一樣:它揭開了人類有史以來最大的謎團。好吧,也許沒有那么夸張,不過它們的影響至少和當年發現負數有平方根不相上下。
我們對函數的理解只有一半是正確的,因為這樣的理解基于一個錯誤的假設:函數一定要把其返回值返回給調用者。按照這樣的理解,continuation就是更加廣義的函數。這里的函數不一定要把返回值傳回給調用者,相反,它可以把返回值傳給程序中的任意代碼。continuation就是一種特別的參數,把這種參數傳到函數中,函數就能夠根據continuation將返回值傳遞到程序中的某段代碼中。說得很高深,實際上沒那么復雜。直接來看看下面的例子好了:
~~~
int i = add(5, 10);
int j = square(i);
~~~
add這個函數將返回15然后這個值會賦給i,這也是add被調用的地方。接下來i的值又會被用于調用square。請注意支持惰性求值的編譯器是不能打亂這段代碼執行順序的,因為第二個函數的執行依賴于第一個函數成功執行并返回結果。這段代碼可以用Continuation Pass Style(CPS)技術重寫,這樣一來add的返回值就不是傳給其調用者,而是直接傳到square里去了。
~~~
int j = add(5, 10, square);
~~~
在上例中,add多了一個參數:一個函數,add必須在完成自己的計算后,調用這個函數并把結果傳給它。這時square就是add的一個continuation。上面兩段程序中j的值都是225。
這樣,我們學習到了強制惰性語言順序執行兩個表達式的第一個技巧。再來看看下面IO程序(是不是有點眼熟?):
~~~
System.out.println("Please enter your name: ");
System.in.readLine();
~~~
這兩行代碼彼此之間沒有依賴關系,因此編譯器可以隨意的重新安排它們的執行順序。可是只要用CPS重寫它,編譯器就必須順序執行了,因為重寫后的代碼存在依賴關系了。
~~~
System.out.println("Please enter your name: ", System.in.readLine);
~~~
這段新的代碼中println需要結合其計算結果調用readLine,然后再返回readLine的返回值。這使得兩個函數得以保證按順序執行而且readLine總被執行(這是由于整個運算需要它的返回值作為最終結果)。Java的println是沒有返回值的,但是如果它可以返回一個能被readnLine接受的抽象值,問題就解決了!(譯者:別忘了,這里作者一開始就在Java的基礎上修改搭建自己的語言)當然,如果一直把函數按照這種方法串下去,代碼很快就變得不可讀了,可是沒有人要求你一定要這樣做。可以通過在語言中添加[語法糖](http://zh.wikipedia.org/wiki/%E8%AF%AD%E6%B3%95%E7%B3%96)的方式來解決這個問題,這樣程序員只要按照順序寫代碼,編譯器負責自動把它們串起來就好了。于是就可以任意安排代碼的執行順序而不用擔心會失去FP帶來的好處了(包括可以用數學方法來分析我們的程序)!如果到這里還有人感到困惑,可以這樣理解,函數只是有唯一成員的類的實例而已。試著重寫上面兩行程序,讓println和readLine編程這種類的實例,所有問題就都搞清楚了。
到這里本章基本可以結束了,而我們僅僅了解到continuation的一點皮毛,對它的用途也知之甚少。我們可以用CPS完成整個程序,程序里所有的函數都有一個額外的continuation作為參數接受其他函數的返回值。還可以把任何程序轉換為CPS的,需要做的只是把當中的函數看作是特殊的continuation(總是將返回值傳給調用者的continuation)就可以了,簡單到完全可以由工具自動完成(史上很多編譯器就是這樣做的)。
一旦將程序轉為CPS的風格,有些事情就變得顯而易見了:每一條指令都會有一些continuation,都會將它的計算結果傳給某一個函數并調用它,在一個普通的程序中這個函數就是該指令被調用并且返回的地方。隨便找個之前提到過的代碼,比如說add(5,10)好了。如果add屬于一個用CPS風格寫出的程序,add的continuation很明顯就是當它執行結束后要調用的那個函數。可是在一個非CPS的程序中,add的continuation又是什么呢?當然我們還是可以把這段程序轉成CPS的,可是有必要這樣做嗎?
事實上沒有必要。注意觀察整個CPS轉換過程,如果有人嘗試要為CPS程序寫編譯器并且認真思考過就會發現:CPS的程序是不需要棧的!在這里完全沒有函數需要做傳統意義上的“返回”操作,函數執行完后僅需要接著調用另外一個函數就可以了。于是就不需要在每次調用函數的時候把參數壓棧再將它們從中取出,只要把這些參數存放在一片內存中然后使用跳轉指令就解決問題了。也完全不需要保留原來的參數:因為這種程序里的函數都不返回,所以它們不會被用第二次!
簡單點說呢,用CPS風格寫出來的程序不需要棧,但是每次調用函數的時候都會要多加一個參數。非CPS風格的程序不需要額外的參數但又需要棧才能運行。棧里面存的是什么?僅僅是參數還有一個供函數運行結束后返回的程序指針而已。這個時候你是不是已經恍然大悟了?對啊,棧里面的數據實際上就是continuation的信息!棧上的程序返回指針實質上就是CPS程序中需要調用的下一個函數!想要知道add(5, 10)的continuation是什么?只要看它運行時棧的內容就可以了。
接下來就簡單多了。continuation和棧上指示函數返回地址的指針其實是同一樣東西,只是continuation是顯式的傳遞該地址并且因此代碼就不局限于只能返回到函數被調用的地方了。前面說過,continuation就是函數,而在我們特制的語言中函數就是類的實例,那么可以得知棧上指向函數返回地址的指針和continuation的參數是一樣的,因為我們所謂的函數(就像類的一個實例)其實就是指針。這也意味著在程序運行的任何時候,你都可以得到當前的continuation(就是棧上的信息)。
好了,我們已經搞清楚當前的continuation是什么了。接下來要弄明白它的存在有什么意義。只要得到了當前的continuation并將它保存起來,就相當于保存了程序的當前狀態:在時間軸上把它凍結起來了。這有點像操作系統進入休眠狀態。continuation對象保存了足夠的信息隨時可以從指定的某個狀態繼續運行程序。在切換線程的時候操作系統也是這樣做的。唯一的區別在于它保留了所有的控制權利。當請求某個continuation對象時(在Scheme語言中是通過調用call-with-current-continuation函數實現的)得到的是一個存有當前continuation的對象,也就是棧對象(在CPS中也就是下一個要執行的函數)。可以把這個對象保存做一個變量中(或者是存在磁盤上)。當以該continuation對象“重啟”該程序時,程序的狀態就會立即“轉換”為該對象中保存的狀態。這一點和切換回一個被暫停的線程或是從系統休眠中喚醒很相像,唯一不同的是continuatoin對象可以反復的這樣使用。當系統喚醒后,休眠前保存的信息就會銷毀,否則你也可以反復的從該點喚醒系統,就像乘時光機回到過去一樣。有了continuation你就可以做到這一點!
那么continuation在什么情況下有用呢?有一些應用程序天生就沒有狀態,如果要在這樣的系統中模擬出狀態以簡化工作的時候,就可以用到continuation。最合適的應用場合之一就是網頁應用程序。微軟的ASP.NET為了讓程序員更輕松的編寫應用程序,花了大量的精力去模擬各種狀態。假如C#支持continuation的話,那么ASP.NET的復雜度將減半:因為只要把某一時刻的continuation保存起來,下次用戶再次發起同樣請求的時候,重新載入這個continuation即可。對于網絡應用的程序員來說就再也沒有中斷了:輕輕松松程序就從下一行開始繼續運行了!對于一些實際問題來說,continuation是一種非常有用的抽象工具。如今大量的傳統胖客戶端(見[瘦客戶端](http://zh.wikipedia.org/wiki/%E7%98%A6%E5%AE%A2%E6%88%B7%E7%AB%AF))正紛紛走進網絡,continuation在未來將扮演越來越重要的角色。