總是有人喜歡爭論這類問題,到底是“函數式編程”(FP)好,還是“面向對象編程”(OOP)好。既然出了兩個幫派,就有人積極地做它們的幫眾,互相唾罵和鄙視。然后呢又出了一個“好好先生幫”,這個幫的人喜歡說,管它什么范式呢,能解決問題的工具就是好工具!我個人其實不屬于這三幫人中的任何一個。
面向對象編程(Object-Oriented Programming)
如果你看透了表面現象就會發現,其實“面向對象編程”本身沒有引入很多新東西。所謂“面向對象語言”,其實就是經典的“過程式語言”(比如Pascal),加上一點抽象能力。所謂“類”和“對象”,基本是過程式語言里面的記錄(record,或者叫結構,structure),它本質其實是一個從名字到數據的“映射表”(map)。你可以用名字從這個表里面提取相應的數據。比如point.x,就是用名字x從記錄point里面提取相應的數據。這比起數組來是一件很方便的事情,因為你不需要記住存放數據的下標。即使你插入了新的數據成員,仍然可以用原來的名字來訪問已有的數據,而不用擔心下標錯位的問題。
所謂“對象思想”(區別于“面向對象”),實際上就是對這種數據訪問方式的進一步抽象。一個經典的例子就是平面點的數據結構。如果你把一個點存儲為:
struct Point { double x; double y; }
那么你用point.x和point.y可以直接訪問它的X和Y坐標。但你也可以把它存儲為“極坐標”方式:
struct Point { double r; double angle; }
這樣你可以用point.r和point.angle訪問它的模和角度。可是現在問題來了,如果你的代碼開頭把Point定義為第一種XY的方式,使用point.x, point.y訪問X和Y坐標,可是后來你決定改變Point的存儲方式,用極坐標,你卻不想修改已有的含有point.x和point.y的代碼,怎么辦呢?
這就是“對象思想”的價值,它讓你可以通過“間接”(indirection,或者叫做“抽象”)來改變point.x和point.y的語義,從而讓使用者的代碼完全不用修改。雖然你的實際數據結構里面根本沒有x和y這兩個成員,但由于.x和.y可以被重新定義,所以你可以通過改變.x和.y的定義來“模擬”它們。在你使用point.x和point.y的時候,系統內部其實在運行兩片代碼,它們的作用是從r和angle計算出x和y的值。這樣你的代碼就感覺x和y是實際存在的成員一樣,而其實它們是被臨時算出來的。在Python之類的語言里面,你可以通過定義“property”來直接改變point.x和point.y的語義。在Java里稍微麻煩一些,你需要使用point.getX()和point.getY()這樣的寫法。然而它們最后的目的其實都是一樣的——它們為數據訪問提供了一層“間接”(抽象)。
這種抽象有時候是個好主意,它甚至可以跟量子力學的所謂“不可觀測性”扯上關系。你覺得這個原子里面有10個電子?也許它們只是像point.x給你的幻覺一樣,也許宇宙里根本就沒有電子這種東西,也許你每次看到所謂的電子,它都是臨時生成出來逗你玩的呢?然而,對象思想的價值也就到此為止了。你見過的所謂“面向對象思想”,幾乎無一例外可以從這個想法推廣出來。面向對象語言的絕大部分特性,其實是過程式語言早就提供的。因此我覺得,其實沒有語言可以叫做“面向對象語言”。就像一個人為一個公司貢獻了一點點代碼,并不足以讓公司以他的名字命名一樣。
“對象思想”作為數據訪問的方式,是有一定好處的。然而“面向對象”(多了“面向”兩個字),就是把這種本來良好的思想東拉西扯,牽強附會,發揮過了頭。很多面向對象語言號稱“所有東西都是對象”(Everything is an Object),把所有函數都放進所謂對象里面,叫做“方法”(method),把普通的函數叫做“靜態方法”(static method)。實際上呢,就像我之前的例子,只有極少需要抽象的時候,你需要使用內嵌于對象之內,跟數據緊密結合的“方法”。其他的時候,你其實只是想表達數據之間的變換操作,這些完全可以用普通的函數表達,而且這樣做更加簡單和直接。這種把所有函數放進方法的做法是本末倒置的,因為函數其實并不屬于對象。絕大部分函數是獨立于對象的,它們不能被叫做“方法”。強制把所有函數放進它們本來不屬于的對象里面,把它們全都作為“方法”,導致了面向對象代碼邏輯過度復雜。很簡單的想法,非得繞好多道彎子才能表達清楚。很多時候這就像把自己的頭塞進屁股里面。
這就是為什么我喜歡開玩笑說,面向對象編程就像“地平說”(Flat Earth Theory)。當然你可以說地球是一個平面。對于局部的,小規模的現象,它沒有問題。然而對于通用的,大規模的情況,它卻不是自然,簡單和直接的。直到今天,你仍然可以無止境的尋找證據,扭曲各種物理定律,自圓其說地平說的幻覺,然而這會讓你的理論非常復雜,經常需要縫縫補補還難以理解。
面向對象語言不僅有自身的根本性錯誤,而且由于面向對象語言的設計者們常常是半路出家,沒有受到過嚴格的語言理論和設計訓練卻又自命不凡,所以經常搞出另外一些奇葩的東西。比如在Javascript里面,每個函數同時又可以作為構造函數(constructor),所以每個函數里面都隱含了一個this變量,你嵌套多層對象和函數的時候就發現沒法訪問外層的this,非得bind一下。Python的變量定義和賦值不分,所以你需要訪問全局變量的時候得用global關鍵字,后來又發現如果要訪問“中間層”的變量,沒有辦法了,所以又加了個nonlocal關鍵字。Ruby先后出現過四種類似lambda的東西,每個都有自己的怪癖…… 有些人問我為什么有些語言設計成那個樣子,我只能說,很多語言設計者其實根本不知道自己在干什么!
軟件領域就是喜歡制造宗派。“面向對象”當年就是乘火打劫,扯著各種幌子,成為了一種宗派,給很多人洗了腦。到底什么樣的語言才算是“面向對象語言”?這樣基本的問題至今沒有確切的答案,足以說明所謂面向對象,基本都是扯淡。每當你指出某個OO語言X的弊端,就會有人跟你說,其實X不是“地道的”OO語言,你應該去看看另外一個OO語言Y。等你發現Y也有問題,有人又會讓你去看Z…… 直到最后,他們告訴你,只有Smalltalk才是地道的OO語言。這不是很搞笑嗎,說一個根本沒人用的語言才是地道的OO語言,這就像在說只有死人的話才是對的。這就像是一群政客在踢皮球,推卸責任。等你真正看看Smalltalk才發現,其實面向對象語言的根本毛病就是由它而來的,Smalltalk并不是很好的語言。很多人至今不知道自己所用的“面向對象語言”里面的很多優點,都是從過程式語言繼承來的。每當發生函數式與面向對象式語言的口水戰,都會有面向對象的幫眾拿出這些過程式語言早就有的優點來進行反駁:“你說面向對象不好,看它能做這個……” 拿別人的優點撐起自己的門面,卻看不到事物實質的優點,這樣的辯論純粹是雞同鴨講。
函數式編程(Functional Programming)
函數式語言一直以來比較低調,直到最近由于并發計算編程瓶頸的出現,以及Haskell,Scala之類語言社區的大力鼓吹,它忽然變成了一種宗派。有人盲目的相信函數式編程能夠奇跡般的解決并發計算的難題,而看不到實質存在的,獨立于語言的問題。被函數式語言洗腦的幫眾,喜歡否定其它語言的一切,看低其它程序員。特別是有些初學編程的人,儼然把函數式編程當成了一天瘦二十斤的減肥神藥,以為自己從函數式語言入手,就可以對經驗超過他十年以上的老程序員說三道四,仿佛別人不用函數式語言就什么都不懂一樣。
函數式編程的優點
函數式編程當然提供了它自己的價值。函數式編程相對于面向對象最大的價值,莫過于對于函數的正確理解。在函數式語言里面,函數是“一類公民”(first-class)。它們可以像1, 2, "hello",true,對象…… 之類的“值”一樣,在任意位置誕生,通過變量,參數和數據結構傳遞到其它地方,可以在任何位置被調用。這些是很多過程式語言和面向對象語言做不到的事情。很多所謂“面向對象設計模式”(design pattern),都是因為面向對象語言沒有first-class function,所以導致了每個函數必須被包在一個對象里面才能傳遞到其它地方。
函數式編程的另一個貢獻,是它們的類型系統。函數式語言對于類型的思維,往往非常的嚴密。函數式語言的類型系統,往往比面向對象語言來得嚴密和簡單很多,它們可以幫助你對程序進行嚴密的邏輯推理。然而類型系統一是把雙刃劍,如果你對它看得太重,它反而會帶來不必要的復雜性和過度工程。這個我在下面講講。
各種“白象”(white elephant)
所謂白象,“white elephant”,是指被人奉為神圣,價格昂貴,卻沒有實際用處的東西。函數式語言里面有很好的東西,然而它們里面有很多多余的特性,這些特性跟白象的性質類似。
函數式語言的“擁護者”們,往往認為這個世界本來應該是“純”(pure)的,不應該有任何“副作用”。他們把一切的“賦值操作”看成低級弱智的作法。他們很在乎所謂尾遞歸,類型推導,fold,currying,maybe type等等。他們以自己能寫出使用這些特性的代碼為豪。可是殊不知,那些東西其實除了能自我安慰,制造高人一等的幻覺,并不一定能帶來真正優秀可靠的代碼。
純函數
半壺水都喜歡響叮當。很多喜歡自吹為“函數式程序員”的人,往往并不真的理解函數式語言的本質。他們一旦看到過程式語言的寫法就嗤之以鼻。比如以下這個C函數:
int f(int x) {
int y = 0;
int z = 0;
y = 2 * x;
z = y + 1;
return z / 3;
}
很多函數式程序員可能看到那幾個賦值操作就皺起眉頭,然而他們看不到的是,這是一個真正意義上的“純函數”,它在本質上跟Haskell之類語言的函數是一樣的,也許還更加優雅一些。
盲目鄙視賦值操作的人,也不理解“數據流”的概念。其實不管是對局部變量賦值還是把它們作為參數傳遞,其實本質上都像是把一個東西放進一個管道,或者把一個電信號放在一根導線上,只不過這個管道或者導線,在不同的語言范式里放置的方向和樣式有一點不同而已!