2011年4月28日 星期四

16. Scala 與新的語言功能 --Continuation

Continuation(延續性)是可以讓程式暫時停止,然後在有需要的時候,再繼續執行。這是一個非常有用的特性,尤其運用在 web 程式的開發。

在 web 程式開發,通常一個功能需要使用者好幾次使用網頁輸入,程式執行中間需要等待使用者的回應。
我們舉一個簡單案例,在 web 製作假單申請這個功能,我們的程式流程可能定義如下(假設使用者已正常登入)

1.畫面顯示假別供使用者選擇想要申請的假別
2.畫面顯示請使用者輸入假期的區間
3.畫面回應使用者申請成功與否

這是一個非常簡單的 web 應用,但實做卻顯得繁瑣。
現在常見的作法是由畫面串連到下一個畫面。
第一個畫面輸入完畢之後,由第一個畫面串連到第二個畫面上,第二個畫面再串到第三個畫面。你的程式是否也是這樣設計?

這樣的解決方案,最麻煩的地方是流程稍微有點更動,就需要找到畫面程式加以修正,對於以後的擴充與維護造成相當大的困擾。

「那是否有比較好的方式?」

有!就是這裡所提的 continuation。

以上例而言,我們可以有一個流程程式,當流程程式執行時,先丟出第一個畫面給用戶,這時流程程式進入等待階段,等待用戶的輸入。
等待階段並不會浪費任何的 thread 與程式資源。當用戶寫完第二個畫面的資料後,會再重新啟動流程程式繼續執行,此時流程程式會依照流程的定義帶出第三個畫面給用戶輸入,依此進行。

使用 continuation 機制製作的程式,控制 web 畫面的是那個流程程式,所以畫面與畫面將不會有串連的問題,未來要修正畫面或修正流程將會非常方便。

另一個 continuation 常用的地方是 network 程式開發,網路程式通常也需要等待對方的回應,才會進行下一步的動作,所以很適合使用 continuation。

對於網路程式的開發,為讓 server 端的程式可以服務多個 client,開發方式有以下的演進

1.最早的處理方式是當有新 client 連接進來時,就 fork 出一個新的 process 來處理該 client 相關的事情,這時這個 process 只處理一個 client,程式可以使用 sequential 的方式處理,不會有問題。
這種模式稱為Process-Based的Server,早期的 Apache Server 主要的處理方式就是採取這種模式。

2.由於啟動一個 process(這是 OS 的 process),所以 server 開銷相當大,以至於 server 無法負擔太多的 client,因此修正為在同一個 process 中啟動不同的 thread 來處理個別的 client。
早期的Java network程式,這是最主要的處理方式。

3.在整臺機器中,thread 終究還是有限制的資源,為應付更多的 client,event-based 的處理模式被引進 Java 中。在 Java 我們常看到的 nio package 就是這種。nio代表 non-block IO,是當網路有資料要讀或要送時,才會回頭呼叫我們的程式,現在 Java 已經進步到 nio2。

nio模式的困難點在於你很難寫一個以 sequential 邏輯為主體的網路程式。
programmer 被迫要一次處理所有的 client,眼光就是一次處理所有的 client,需要將原來網路程式的各個步驟紀錄在不同的資料中,然後依照不同的 client 適時呼叫不同的模組,很麻煩。

為何我們不能像以前開發單一 client 的方式一樣,將處理的眼光只侷限在一個 client 就好。這就是 continuation 運用在 network 程式的方式。
當 nio callback 到我們的程式時,我們只需要該 client 所對應到的 continuation 程式叫起,然後要求該 continuation 程式繼續執行,就完成了。

以這種概念開發程式最重要的地方在於 continuation 機制的完整與否。

Continuation 可以讓我們製作一些程式時很簡單,但簡單的前提是要有好的 continuation 機制。很可惜的,在 Java 世界中現在沒有比較好 continuation 機制。
Java 世界確實存在一些 continuation 機制,但這些機制不是設定方式過於複雜就是實用性不好。有一些 web framework 號稱有 flow 或是 web flow 的機制,讓人燃起一些希望,但仔細瞭解後卻感到實用性不大。

嚴謹來說,在 Java 世界,根本沒有完整的 continuation 機制,最多只是半成品,對於有這麼龐大資源、framework 滿坑滿谷的 Java 世界,實在讓人訝異。

Continuation(延續性)很早就出現在 Smalltalk 中。由於 web 應用的特性,所以有幾個 web framework。相繼支援continuation,比如 Ruby On Rail (ROR)。

在幾年前 ROR 贏得眾人眼光之際,有人提出 Java 支援 continuation 的需求,比如有名的 Jetty Web Server 非常努力爭取。那時的 Java 團隊壓力不小,也經過許多次會議,但最後決議當然是「不支援」。

經過前篇 closure 的討論之後,我相信各位讀者都很清楚,要在 Java 這個老舊的機器加上其他強大的功能不是太容易,所以這個不支援 continuation 的決議,應該也不會太令人震驚。

各位讀者,可能會認為進到  Scala 這個世界,continuation 就自然垂手可得,但很不幸,這次你要有點失望了。
沒有說失望是因為 Scala 確實有支援,但有點失望是因為 Scala 支援 continuation 的方式令人有點不敢恭維。

Scala 的 continuation 方式有它的學理根據,它使用 CPS (Continuation Passing Style) 的方式來製作 continuation,Scala 又稱呼這種模式的 continuation 叫組合式的 continuation (Composable continuation)或 delimited continuation(意思為:一個continuation是各個段落所組成起來的)。

CPS 的意思簡單來說,就是「執行一段程式時,執行到某個地方,暫停後,將剩下的程式碼移轉出去。這個移轉出去的程式碼,在下次需要時,再將它啟動繼續執行」。
這個「暫停後,將剩下程式移轉,之後繼續執行剩下的這段程式碼」,就很像是一般我們對 continuation 的認知,可達到我們的需求。

這種 CPS 的技巧常見於函數式語言中,由於函數式語言中的函數,可以是函數組成函數,所以很容易使用函數的組合來製作這種組合式的 continuation。

Scala 基本上完全遵循函數式語言的這種模式,因此可達到 continuation 的需求。Scala 使用Responder 這個 class 輔助你撰寫 continuation,看看以下的例子:

object Test2 extends Application {
def sumup(x: int): Responder[int] = 
    for {
      val line <- ask("enter a number");
      val rest <- if (line == "") constant(x) 
                  else if (line == "?") { println("sum so far: " + x); sumup(x) }
                  else sumup(x + Integer.parseInt(line))
    } yield rest
  println("sum = " + run(sumup(0)).get)
}

for 裡面的程式碼是 continuation 的部份,可以讓你暫停程式,讀取資料後,再進行。
要注意的是,這裡的 for 並不是一般 Scala 看到 iteration 的意思,實在是令人驚訝的使用方式。

Scala 的 continuation,學理很精妙,但實用起來,國外有人說會讓人頭腦炸掉。筆者認為講得不無道理。很難要求每個人的頭腦都像 Martin 等人都有大師級的能耐。

若是你有一些專案需要使用 continuation,除非你是大師級的人物,或是很有毅力且有足夠時間的人,那你可以嘗試 Scala 的continuation。至於其他,也許再等等看以後的機會。

若你的專案很趕,那筆者建議你用原來處理流程的方式來處理你的問題(畫面串畫面不是好事,但也可以),不要過於寄望 Scala 的 continuation。

參考資料

沒有留言:

張貼留言