函數(shù)式語言的宗教
by wangyin
很早的時候,“函數(shù)式語言”對于我來說就是 Lisp,因為 Lisp 可以在程序的幾乎任意位置定義函數(shù),并且把它們作為值來傳遞(這叫做 first-class function)??墒堑胶髞碛腥烁嬖V我,Lisp 其實不算“函數(shù)式語言”,因為 Lisp 的函數(shù)不“純”(pure)。
所謂“純函數(shù)”,就是像數(shù)學函數(shù)一樣,你給它同樣的輸入,它就給你同樣的輸出。然后你就發(fā)現(xiàn)在這種定義下,幾乎所有程序語言里面的隨機數(shù)函數(shù)(random
),都不是“純函數(shù)”。因為每一次調(diào)用?random()
,輸入都是一樣的(沒有參數(shù)),但每次會輸出不同的隨機數(shù)。他們告訴我,不純的函數(shù)容易出錯,沒法驗證它的正確性。
在這種害怕自己用的語言“不純”,不安全的恐慌之下,我開始接觸 Haskell,一種號稱“純函數(shù)式”,安全的語言。我深信 Haskell 的純函數(shù)教條長達幾年之久,對其它語言里的“副作用”(side-effect)嗤之以鼻。我認為宇宙的本質(zhì)是純的,數(shù)學就是宇宙的終極語言,所以程序語言也應該像數(shù)學一樣純粹……
你有沒有意識到,所有的邪教頭子最初都是利用了人們的恐懼心理,進而讓他們深信不疑,以為遇到了救世主的?當我多次碰壁,猛然醒悟的時候,我發(fā)現(xiàn) Haskell 就是這樣一種邪教 :p
每一種宗教都有一個神秘的,體現(xiàn)自己“精神”的徽標。Haskell 的是這個樣子:

當一個初學者進入 Haskell 的 IRC 聊天室的時候,老手們會極其耐心地問答他的問題。他們告訴他,Haskell 社區(qū)是最友好,最不宗教,最講科學和理性的社區(qū)??墒蔷枚弥?,你發(fā)現(xiàn)其實不是那個樣子。他們對你友好,當且僅當你沒有指出 Haskell 的致命缺陷。如果你知道了那些缺陷,要跟他們討論的時候,就會發(fā)現(xiàn)一切都變了。他們會說,你懂不起!我們是科學家!如果你不承認這一點,我們就殺了你!:p
Haskell 的社區(qū)喜歡在他們的概念里省掉“純”這個字,把 Haskell 叫做“函數(shù)式語言”。他們喜歡“糾正”別人的概念。他們告訴人們,“不純”的函數(shù)式語言,其實都不配叫做“函數(shù)式語言”。在他們的這種定義下,Lisp 這么老牌的函數(shù)式語言,居然都不能叫“函數(shù)式語言”了。但是看完這篇文章你就會發(fā)現(xiàn),其實他們的這種定義是狹隘和錯誤的。
在 Haskell 里面,你不能使用通常語言里面都有的賦值語句,比如 Pascal 里的?x:=1
,C 和 Java 里的?x=1
,或者 Scheme 里的?(set! x 1)
,Common Lisp 里的?(setq x 1)
。這樣一來,你就不可能保留“狀態(tài)”(state)。所謂“狀態(tài)”,就是指“隨機數(shù)種子”那樣的東西,其實本質(zhì)上就是“全局變量”。比如,在 C 語言里定義?random()
?函數(shù),你可以這么做:
int random()
{
?
????static int seed = 0;
?
????seed = next_random(seed);
?
????return seed;
} ? ?
這里的?seed
?是一個“static 變量”,其本質(zhì)就是一個全局變量,只不過這個全局變量只能被?random
?這一個函數(shù)訪問。每次調(diào)用?random()
,它都會使用?next_random(seed)
?生成下一個隨機數(shù),并且把?seed
?的值更新為這個新的隨機數(shù)。在?random()
?的執(zhí)行結(jié)束之后,seed
?會一直保存這個值。下一次調(diào)用?random()
,它就會根據(jù)?seed
?保存的值,算出下一個隨機數(shù),然后再次更新?seed
,如此繼續(xù)。這就是為什么每一次調(diào)用?random()
,你都會得到不同的隨機數(shù)。
可是在 Haskell 里面情況就很不一樣了。由于 Haskell 不能保留狀態(tài),所以同一個“變量”在它作用域的任何位置都具有相同的值。每一個函數(shù)只要輸入相同,就會輸出同樣的結(jié)果。所以在 Haskell 里面,你不能輕松的表達?random
?這樣的“不純函數(shù)”。為了讓?random
?在每次調(diào)用得到不同的輸出,你必須給它“不同的輸入”。那怎么才能給它不同的輸入呢?Haskell 采用的辦法,就是把“種子”作為輸入,然后返回兩個值:新的隨機數(shù)和新的種子,然后想辦法把這個新的種子傳遞給下一次的?random
?調(diào)用。所以 Haskell 的?random
?的“線路”看起來像這個樣子:
(舊種子)---> (新隨機數(shù),新種子)
現(xiàn)在問題來了。得到的這個新種子,必須被準確無誤的傳遞到下一個使用?random
?的地方,否則你就沒法生成下一個隨機數(shù)。因為沒有地方可以讓你“暫存”這個種子,所以為了把種子傳遞到下一個使用它的地方,你經(jīng)常需要讓種子“穿過”一系列的函數(shù),才能到達目的地。種子經(jīng)過的“路徑”上的所有函數(shù),必須增加一個參數(shù)(舊種子),并且增加一個返回值(新種子)。這就像是用一根吸管扎穿這個函數(shù),兩頭通風,這樣種子就可以不受干擾的通過。
所以你看到了,為了達到“純函數(shù)”的目標,我們需要做很多“管道工”的工作,這增加了程序的復雜性和工作量。如果我們可以把種子存放在一個全局變量里,到需要的時候才去取,那就根本不需要把它傳來傳去的。除?random()
?之外的代碼,都不需要知道種子的存在。
為了減輕視覺負擔和維護這些進進出出的“狀態(tài)”,Haskell 引入了一種叫 monad 的概念。它的本質(zhì)是使用類型系統(tǒng)的“重載”(overloading),把這些多出來的參數(shù)和返回值,掩蓋在類型里面。這就像把亂七八糟的電線塞進了接線盒似的,雖然表面上看起來清爽了一些,底下的復雜性卻是不可能消除的。有時候我很納悶,在其它語言里易如反掌的事情,為什么到 Haskell 里面就變成了“研究性問題”,很多時候就是 monad 這東西在搗鬼。特別是當你有多個“狀態(tài)”的時候,你就需要使用像 monad transformer 這樣的東西。而 monad transformer 在本質(zhì)上其實是一個丑陋的 hack,它并不能從根本上解決問題,卻可以讓你傷透腦筋也寫不出來。有些人以為會用 monad 和 monad transformer 就說明他水平高,其實這根本就是自己跟自己過不去而已。
當談到 monad 的時候,我喜歡打這樣一個比方:
使用含有 monad 的“純函數(shù)式語言”,就像生活在一個沒有電磁波的世界。
在這個世界里面沒有收音機,沒有手機,沒有衛(wèi)星電視,沒有無線網(wǎng),甚至沒有光!這個世界里的所有東西都是“有線”的。你需要絞盡腦汁,把這些電線準確無誤的通過特殊的“接線器”(monad)連接起來,才能讓你的各種信息處理設(shè)備能夠正常工作,才能讓你自己能夠看見東西。如果你想生活在這樣的世界里的話,那就請繼續(xù)使用 Haskell。
其實要達到純函數(shù)式語言的這種“純”的效果,你根本不需要使用像 Haskell 這樣完全排斥“賦值語句”的語言。你甚至不需要使用 Lisp 這樣的“非純”函數(shù)式語言。你完全可以用 C 語言,甚至匯編語言,達到同樣的效果。
我只舉一個非常簡單的例子,在 C 語言里面定義如下的函數(shù)。雖然函數(shù)體里面含有賦值語句,它卻是一個真正意義上的“純函數(shù)”:
int f(int x) {
? ?
????int y = 0;
? ?
????int z = 0;
? ?
????y = 2 * x;
? ?
????z = y + 1;
? ?
????return z / 3;
} ? ?
這是為什么呢?因為它計算的是數(shù)學函數(shù)?f(x) = (2x+1)/3
?。你給它同樣的輸入,肯定會得到同樣的輸出。函數(shù)里雖然對?y
?和?z
?進行了賦值,但這種賦值都是“局部”的,它們不會留下“狀態(tài)”。所以這個函數(shù)雖然使用了被“純函數(shù)程序員”們唾棄的賦值語句,卻仍然完全的符合“純函數(shù)”的定義。
如果你研究過編譯器,就會理解其中的道理。因為這個函數(shù)里的?y
?和?z
,不過是函數(shù)的“數(shù)據(jù)流”里的一些“中間節(jié)點”,它們的用途是用來暫存一些“中間結(jié)果”。這些局部的賦值操作,跟函數(shù)調(diào)用時的“參數(shù)傳遞”沒有本質(zhì)的區(qū)別,它們不過都是把信息傳送到指定的節(jié)點而已。如果你不相信的話,我現(xiàn)在就可以把這些賦值語句全都改寫成函數(shù)調(diào)用:
int f(int x) {
??
????return g(2 * x);
}?
int g(int y) {
? ?
????return h(y + 1);
}?
int h(int z) {
? ?
return z/3;
} ? ?
很顯然,這兩個 f 的定義是完全等價的,然而第二個定義卻沒有任何賦值語句。第一個函數(shù)里對?y
?和?z
?的“賦值語句”,被轉(zhuǎn)換成了等價的“參數(shù)傳遞”。這兩個程序如果經(jīng)過我寫的編譯器,會生成一模一樣的機器代碼。所以如果你說賦值語句是錯誤的話,那么函數(shù)調(diào)用也應該是錯誤的了。那我們還要不要寫程序了?
盲目的排斥賦值語句,來自于對“純函數(shù)”這個概念的片面理解。很多研究像 Haskell,ML 一類語言的專家,其實并不明白我上面講的道理。他們仿佛覺得如果使用了賦值,函數(shù)就肯定不“純”了似的。CMU 的教授 Robert Harper 就是這樣一個極端。他在一篇博文里指出,人們不應該把程序里的“變量”叫做“變量”,因為它跟數(shù)學和邏輯學里所謂的“變量”不是一回事,它可以被賦值。然而,其果真如他所說的那樣嗎?如果你理解了我對上面的例子的分析,你就會發(fā)現(xiàn)其實程序里的“變量”,跟數(shù)學和邏輯學里面的“變量”相比,其實并沒有本質(zhì)的不同。
程序里的變量甚至更加嚴格一些。如果你把數(shù)學看作一種程序語言的話,恐怕沒有一本數(shù)學書可以編譯通過。因為它們里面充滿了變量名沖突,未定義變量,類型錯誤等程序設(shè)計的低級錯誤。你只需要注意概率論里表示隨機數(shù)的大寫變量(比如 X),就會發(fā)現(xiàn)數(shù)學所謂的“變量”其實是多么的不嚴謹。這變量 X 根本不需要被賦值,它自己身上就帶“副作用”!實際上,90%以上的數(shù)學家都寫不出像樣的程序來。所以拿數(shù)學的“變量”來衡量程序語言的“變量”,其實是顛倒了。我們應該用程序的“變量”來衡量數(shù)學的“變量”,這樣數(shù)學的語言才會有所改善。
邏輯學家雖然有他們的價值,但他們并不是先知,并不總是對的。由于沉迷于對符號的熱愛,他們經(jīng)??床坏绞挛锏谋举|(zhì)。雖然他們理解很多符號公式和推理規(guī)則,但他們卻經(jīng)常不明白這些符號和推理規(guī)則,到底代表著自然界中的什么物體,所以有時候他們連最基本的問題都會搞錯(比如他們有時候會混淆“全稱量詞”?的作用域)。邏輯學家們的教條主義和崇古作風,也許就是圖靈當年在 Church 手下做學生那么孤立,那么痛苦的原因。也就是這個圖靈,在某種程度上超越了 Church,把一部分人從邏輯學的死板思維模式下解放了出來,變成了“計算機科學家”。當然其中某些計算機科學家墮入了另外一種極端,他們對邏輯學已有的精華一無所知,所以搞出一些完全沒有原則的設(shè)計,然而這不是這篇文章的主題。
所以綜上所述,我們完全沒有必要追求什么“純函數(shù)式語言”,因為我們可以在不引起混淆的前提下使用賦值語句,而寫出真正的“純函數(shù)”來??梢宰杂傻膶ψ兞窟M行賦值的語言,其實超越了通常的數(shù)理邏輯的表達能力。如果你不相信這一點,就請想一想,數(shù)理邏輯的公式有沒有能力推斷出明天的天氣?為什么天氣預報都是用程序算出來的,而不是用邏輯公式推出來的?所以我認為,程序其實在某種程度上已經(jīng)成為比數(shù)理邏輯更加強大的邏輯。完全用數(shù)理邏輯的思維方式來對程序語言做出評價,其實是很片面的。
說了這么多,對于“函數(shù)式語言”這一概念的誤解,應該消除得差不多了。其實“函數(shù)式語言”唯一的要求,應該是能夠在任意位置定義函數(shù),并且能夠把函數(shù)作為值傳遞,不管這函數(shù)是“純”的還是“不純”的。所以像 Lisp 和 ML 這樣的語言,其實完全符合“函數(shù)式語言”這一稱號。