只要遵守函數編程(FP)的原則,管他用什麼語言,都可以進行FP。你可以用非函數式的語言(例如Java),進行FP;正如同你可以用非物件導向的語言(例如C),進行OOP(物件導向編程)一樣。但是只有想不開的人才會這麼做,畢竟這麼做是事倍功半。
1960年代左右,LISP誕生,被視為第一個函數式語言,從此越來越多函數式語言隨之出現。不過,畢竟Lambda Calculus是讓虛幻不存在的機器執行的,沒有受到真實世界的限制,所以真實世界的函數式語言雖然都是源自於Lambda Calculus,但卻都和Lambda Calculus之間有所差異。
由於FP只是一些構想觀念,各種語言實踐FP的作法也可能有所不同,但是大致上來說,FP的共同點在於:「沒有副作用」(Side Effect)、「第一級函數」(First-Class Function)。前者是指在表示式(Expression)內不可以造成值的改變;後者是指函數被當作一般值對待,而不是次級公民,可當作「傳入參數」或「傳出結果」。
FP和我們慣用的程式編寫風格,有相當大的差異。Imperative Programming(命令式編程)認為程式的執行,就是一連串狀態的改變;但FP則將程式的運作,視為數學函數的計算,並且避免「狀態」和「可變資料」。但是,沒有狀態、沒有可變資料,程式要如何運作呢?事實上,FP使用函數,而函數可以「自動」幫我們保存資料。Imperative Programming的資料大量放在heap中;但FP則是放在堆疊(tack)內(或者由堆疊指向heap)。
在討論FP時,也常常會談到遞迴(Recursion)。為何遞迴對於FP相當重要?因為遞迴可以用來保存狀態。以費伯納西數列(1, 1, 2, 3, 5, 8, 13……)來說,每個值是前兩個值的和,想製造出費伯納西數列,Imperative編程的作法會用迴圈,而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)──你可以不需要關閉軟體系統,直接部署新的程式模組。(未完待續)
作者簡介:
蔡學鏞-技術顧問
清華大學資訊工程碩士,曾任華碩集團軟體工程師、元智大學資訊系講師、美商歐萊禮出版社技術編輯、臺灣微軟特約專欄作家。
熱門新聞
2024-10-30
2024-10-27
2024-10-30
2024-10-29
2024-10-23
2024-10-29