2008年5月29日星期四

思考函數編程(二)Why FP

文 / 蔡學鏞

請先閱讀「思考函數編程(一)Language is Functional Again

只要遵守FP的原則,管他用什麼語言,都可以進行FP。你可以用非函數式的語言(例如Java),進行FP;正如同你可以用非物件導向的語言(例如C),進行OOP一樣。但是只有想不開的人才會這麼做,畢竟事倍功半。

LISP是第一個函數式語言,越來越多函數式語言隨之出現。真實世界的函數式語言無法像Lambda Calculus那樣,畢竟Lambda Calculus是讓虛幻不存在的機器執行的,沒有受到真實世界的限制。所以函數式語言雖然都是源自於Lambda Calculus,但是卻都和Lambda Calculus之間存在差異。由於FP只是一些構想,各種語言實踐這些構想的作法,彼此之間也可能有不小的差異。

儘管各種語言有差異,但是大致上來說,FP的共同點在於:「沒有副作用」(Side Effect)、「第一級函數」(First-Class Function)。「沒有副作用」是指在表示式(expression)內不可以造成值的改變;「第一級函數」是指函數被當作一般值對待,而不是次級公民,也就是說,函數可當作「傳入參數」或「傳出結果」。

基本上,遵守上述兩點進行程式編寫,差不多就可以稱為FP。而且這兩點和OOP是沒有衝突的,所以同時採用OOP和FP的編程風格,是有可能的。儘管FP重度愛好者似乎都對OOP沒有特別的好感,甚至會對OOP口出批評。究竟FP和OOP之間是互補還是競爭,究竟採用FP之後,還有必要使用OOP嗎?這是值得探討的話題。

FP和我們慣用的編寫程式風格,有相當大的差異。Imperative Programming認為程式的執行,就是一連串狀態的改變;但FP將程式的運作,視為數學函數的計算,且避免「狀態」和「可變資料」。但是,沒有狀態,沒有可變資料,程式要如何運作呢?事實上,FP使用函數,而函數可以「自動」幫我們保存資料。Imperative Programming的資料大量放在heap中,但FP則是放在堆疊(stack)內(或者由堆疊指向heap)。而現在普遍流行的GC(垃圾收集),源自於FP語言。

在討論FP時,也常常會討論到遞迴(recursion)。為何遞迴對於FP相當重要?因為遞迴可以用來保存狀態。以費伯納西數列(1, 1, 2, 3, 5, 8, 13…)來說,每個值是前兩個值的和,想製造出費伯納西數列,imperative編程的作法會用迴圈,而FP的作法會用遞迴。

這個時候,你可能會說,用遞迴寫出費伯納西數列或計算階乘,這樣的程式你曾經用C或C++或Java寫過。恭喜你,你確實用過FP,只是你當時不自知而已。

遞迴可以保存狀態,可以讓程式變得相當精簡,但是成本(時間與記憶體)也很高。所以,許多時候,函數式語言會希望我們將程式寫成尾端遞迴(Tail Recursion),以便編譯器自動將它編譯成記憶體的直接跳躍(也就是迴圈)。

為了提昇效率,許多函數式語言會納入imperative的某些作法(例如允許副作用),這類的FPL被稱為不純(Impure)的函數式編程語言,例如Ocaml、F#、LISP、REBOL。當然也有一些語言堅持Pure Functional的作法,例如Erlang、Haskell、Occam、Oz。

以往純的函數式語言會被某些人認為「不食人間煙火」,而不純的函數式語言,則被認為比較實際、實用,但是最近大家的看法似乎有了改變。主要是以Erlang為首的純函數式語言,似乎更能充分展現出FP的優勢。除了可以標新立異當作IT上流社會炫耀表徵之外,究竟採用FP有何優勢?

首先是,單元測試(Unit)變得相當容易。對OOP來說,單元測試是以類別為單元,這種單元其實不小,而且要測試完整也不見得很容易。對於FP來說,函數是單元測試的單位。因為函數不可以有副作用,所以對於函數來說,我們要注意的只有輸入(引數)和輸出(傳出值),且傳出值只受到引數的影響。

這使得單元測試相當容易,只要管引數的結果正確與否就好,不需要管函數呼叫的次序正確與否,或者外部狀態是否做好正確的設定。如果是像C、Java或C#這類語言,檢查函數的傳出值是不夠的,因為函數執行過程中可能會改變外部狀態。但是對於FP來說,就不用擔心這一點。

想除錯,就必須能讓此錯誤可以重現(reproduce),然後定位(locate)錯誤的地方。對FP來說,由於沒有外部狀態的因素干擾,所以上述這兩點都相當容易就可以做到。Erlang的某個函數只要會出錯,就一定每次都會出錯,所以可以「重現」;C語言的某個函數出錯,卻不見得每次都出錯,相當麻煩。一旦知道某個函數出錯,你可以快速地在Erlang函數內找出問題所在,而予以修正;但是對C語言來說,外部狀態影響太多,不容易除錯。

FP相當適合寫(concurrency)的程式。沒有共享記憶體,沒有執行緒,不需要擔心critical section,不必使用mutex等上鎖機制。由於沒有外部狀態的問題,FP的程式也相當適合進行程式碼「熱抽換」或「熱部署」(Hot Code Deployment) -- 你可以不需要關閉你的軟體系統,可以直接部署新的程式模組。(未完待續)