你大概已經(jīng)在想:上面這種怪胎函數(shù)怎么也不合理嘛。在我剛開始學(xué)習(xí)FP的時候我也這樣想的。不過后來我知道我是錯的。使用這種方式編程有很多好處。其中一些是主觀的。比如說有人認為函數(shù)式程序更容易理解。這個我就不說了,哪怕街上隨便找個小孩都知道‘容易理解’是多么主觀的事情。幸運的是,客觀方面的好處還有很多。
因為FP中的每個符號都是final的,于是沒有什么函數(shù)會有副作用。誰也不能在運行時修改任何東西,也沒有函數(shù)可以修改在它的作用域之外修改什么值給其他函數(shù)繼續(xù)使用(在指令式編程中可以用類成員或是全局變量做到)。這意味著決定函數(shù)執(zhí)行結(jié)果的唯一因素就是它的返回值,而影響其返回值的唯一因素就是它的參數(shù)。
這正是單元測試工程師夢寐以求的啊。現(xiàn)在測試程序中的函數(shù)時只需要關(guān)注它的參數(shù)就可以了。完全不需要擔(dān)心函數(shù)調(diào)用的順序,也不用費心設(shè)置外部某些狀態(tài)值。唯一需要做的就是傳遞一些可以代表邊界條件的參數(shù)給這些函數(shù)。相對于指令式編程,如果FP程序中的每一個函數(shù)都能通過單元測試,那么我們對這個軟件的質(zhì)量必將信心百倍。反觀Java或者C++,僅僅檢查函數(shù)的返回值是不夠的:代碼可能修改外部狀態(tài)值,因此我們還需要驗證這些外部的狀態(tài)值的正確性。在FP語言中呢,就完全不需要。
如果一段FP程序沒有按照預(yù)期設(shè)計那樣運行,調(diào)試的工作幾乎不費吹灰之力。這些錯誤是百分之一百可以重現(xiàn)的,因為FP程序中的錯誤不依賴于之前運行過的不相關(guān)的代碼。而在一個指令式程序中,一個bug可能有時能重現(xiàn)而有些時候又不能。因為這些函數(shù)的運行依賴于某些外部狀態(tài), 而這些外部狀態(tài)又需要由某些與這個bug完全不相關(guān)的代碼通過某個特別的執(zhí)行流程才能修改。在FP中這種情況完全不存在:如果一個函數(shù)的返回值出錯了,它一直都會出錯,無論你之前運行了什么代碼。
一旦問題可以重現(xiàn),解決它就變得非常簡單,幾乎就是一段愉悅的旅程。中斷程序的運行,檢查一下棧,就可以看到每一個函數(shù)調(diào)用時使用的每一個參數(shù),這一點和指令式代碼一樣。不同的是指令式程序中這些數(shù)據(jù)還不足夠,因為函數(shù)的運行還可能依賴于成員變量,全局變量,還有其他類的狀態(tài)(而這些狀態(tài)又依賴于類似的變量)。FP中的函數(shù)只依賴于傳給它的參數(shù),而這些參數(shù)就在眼前!還有,對指令式程序中函數(shù)返回值的檢查并不能保證這個函數(shù)是正確運行的。還要逐一檢查若干作用域以外的對象以確保這個函數(shù)沒有對這些牽連的對象做出什么越軌的行為(譯者:好吧,翻譯到這里我自己已經(jīng)有點激動了)。對于一個FP程序,你要做的僅僅是看一下函數(shù)的返回值。
把棧上的數(shù)據(jù)過一遍就可以得知有哪些參數(shù)傳給了什么函數(shù),這些函數(shù)又返回了什么值。當(dāng)一個返回值看起來不對頭的那一刻,跳進這個函數(shù)看看里面發(fā)生了什么。一直重復(fù)跟進下去就可以找到bug的源頭!
不需要任何改動,所有FP程序都是可以并發(fā)執(zhí)行的。由于根本不需要采用鎖機制,因此完全不需要擔(dān)心死鎖或是并發(fā)競爭的發(fā)生。在FP程序中沒有哪個線程可以修改任何數(shù)據(jù),更不用說多線程之間了。這使得我們可以輕松的添加線程,至于那些禍害并發(fā)程序的老問題,想都不用想!
既然是這樣,為什么沒有人在那些高度并行的那些應(yīng)用程序中采用FP編程呢?事實上,這樣的例子并不少見。愛立信開發(fā)了一種FP語言,名叫Erlang,并應(yīng)用在他們的電信交換機上,而這些交換機不僅容錯度高而且拓展性強。許多人看到了Erlang的這些優(yōu)勢也紛紛開始使用這一語言。在這里提到的電信交換控制系統(tǒng)遠遠要比華爾街上使用的系統(tǒng)具有更好的擴展性也更可靠。事實上,用Erlang搭建的系統(tǒng)并不具備可擴展性和可靠性,而Java可以提供這些特性。Erlang只是像巖石一樣結(jié)實不容易出錯而已。
FP關(guān)于并行的優(yōu)勢不僅于此。就算某個FP程序本身只是單線程的,編譯器也可以將其優(yōu)化成可以在多CPU上運行的并發(fā)程序。以下面的程序為例:
String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);
如果是函數(shù)式程序,編譯器就可以對代碼進行分析,然后可能分析出生成字符串s1和s2的兩個函數(shù)可能會比較耗時,進而安排它們并行運行。這在指令式編程中是無法做到的,因為每一個函數(shù)都有可能修改其外部狀態(tài),然后接下來的函數(shù)又可能依賴于這些狀態(tài)的值。在函數(shù)式編程中,自動分析代碼并找到適合并行執(zhí)行的函數(shù)十分簡單,和分析C的內(nèi)聯(lián)函數(shù)沒什么兩樣。從這個角度來說用FP風(fēng)格編寫的程序是“永不過時”的(雖然我一般不喜歡說大話空話,不過這次就算個例外吧)。硬件廠商已經(jīng)沒辦法讓CPU運行得再快了。他們只能靠增加CPU核的數(shù)量然后用并行來提高運算的速度。這些廠商故意忽略一個事實:只有可以并行的軟件才能讓你花大價錢買來的這些硬件物有所值。指令式的軟件中只有很小一部分能做到跨核運行,而所有的函數(shù)式軟件都能實現(xiàn)這一目標,因為FP的程序從一開始就是可以并行運行的。
在Windows早期,如果要更新系統(tǒng)那可是要重啟電腦的,而且還要重啟很多次。哪怕只是安裝一個新版本的播放器。到了XP的時代這種情況得到比較大的改善,盡管還是不理想(我工作的時候用的就是Windows,就在現(xiàn)在,我的系統(tǒng)托盤上就有個討厭的圖標,我不重啟機子就不消失)。這一方面Unix好一些,曾經(jīng)。只需要暫停一些相關(guān)的部件而不是整個操作系統(tǒng),就可以安裝更新了。雖然是要好一些了,對很多服務(wù)器應(yīng)用來說這也還是不能接受的。電信系統(tǒng)要求的是100%的在線率,如果一個救急電話因為系統(tǒng)升級而無法撥通,成千上萬的人就會因此喪命。同樣的,華爾街的那些公司怎么也不能說要安裝軟件而在整個周末停止他們系統(tǒng)的服務(wù)。
最理想的情況是更新相關(guān)的代碼而不用暫停系統(tǒng)的其他部件。對指令性程序來說是不可能的。想想看,試著在系統(tǒng)運行時卸載掉一個Java的類然后再載入這個類的新的實現(xiàn),這樣做的話系統(tǒng)中所有該類的實例都會立刻不能運行,因為該類的相關(guān)狀態(tài)已經(jīng)丟失了。這種情況下可能需絞盡腦汁設(shè)計復(fù)雜的版本控制代碼,需要將所有這種類正在運行的實例序列化,逐一銷毀它們,然后創(chuàng)建新類的實例,將現(xiàn)有數(shù)據(jù)也序列化后裝載到這些新的實例中,最后希望負責(zé)裝載的程序可以正確的把這些數(shù)據(jù)移植到新實例中并正常的工作。這種事很麻煩,每次有新的改動都需要手工編寫裝載程序來完成更新,而且這些裝載程序還要很小心,以免破壞了現(xiàn)有對象之間的聯(lián)系。理論上是沒問題,可是實際上完全行不通。
FP的程序中所有狀態(tài)就是傳給函數(shù)的參數(shù),而參數(shù)都是儲存在棧上的。這一特性讓軟件的熱部署變得十分簡單。只要比較一下正在運行的代碼以及新的代碼獲得一個diff,然后用這個diff更新現(xiàn)有的代碼,新代碼的熱部署就完成了。其它的事情有FP的語言工具自動完成!如果還有人認為這只存在于科幻小說中,他需要再想想:多年來Erlang工程師已經(jīng)使用這種技術(shù)對它們的系統(tǒng)進行升級而完全不用暫停運行了。
FP語言有一個特性很有意思,那就是它們是可以用數(shù)學(xué)方法來分析的。FP語言本身就是形式系統(tǒng)的實現(xiàn),只要是能在紙上寫出來的數(shù)學(xué)運算就可以用這種語言表述出來。于是只要能夠用數(shù)學(xué)方法證明兩段代碼是一致的,編譯器就可以把某段代碼解析成在數(shù)學(xué)上等同的但效率又更高的另外一段代碼7。 關(guān)系數(shù)據(jù)庫已經(jīng)用這種方法進行優(yōu)化很多年了。沒有理由在常規(guī)的軟件行業(yè)就不能應(yīng)用這種技術(shù)。 另外,還可以用這種方法來證明代碼的正確性,甚至可以設(shè)計出能夠自動分析代碼并為單元測試自動生成邊緣測試用例的工具出來!對于那些對缺陷零容忍的系統(tǒng)來說,這一功能簡直就是無價之寶。例如心臟起搏器,例如飛行管控系統(tǒng),這幾乎就是必須滿足的需求。哪怕你正在開發(fā)的程序不是為了完成什么重要核心任務(wù),這些工具也可以幫助你寫出更健壯的程序,直接甩競爭對手n條大街。
更多建議: