對象已死?
最近常有一種說法,就是我們如今面臨著另外一場編程模型的變革,面向對象技術已經處在被淘汰的邊緣,函數式語言會取代面向對象技術成為主流方式,甚至出現了面向對象已死的言論。作為一個硬核函數語言的狂熱者,我個人當然希望函數式語言可以一統天下,成為主流之選。但是不是應該把對象技術和函數技術對立起來,說式后者取前者而代之,我個人認為,這和如何看待面向對象技術有關。
做為工程實踐的對象技術
在這個年代,大家有一種神圣化面向對象技術的傾向,很多人都把對象技術奉為高深的思想和理論。但實際上,面向對象技術僅僅一種工程實踐而已,它是依托于其他技術而存在的一種實踐,本身并不是一種完備的計算模型。
在計算機科學發展的早期,對于計算機的非數值計算應用的討論,以及對于可計算性問題的研究和發展,大抵確立了幾種的計算模型:遞歸函數類、圖靈機、Lambda演算、Horn子句、Post系統等等。其中遞歸函數類是可計算性問題的數學解釋;Horn子句是prolog這類邏輯語言的理論基礎;lambda演算成為了函數式語言的理論基礎;圖靈機是圖靈解決可計算問題的時候所設計的裝置,其后成為計算機的裝置模型,與圖靈機相關的自動機以及馮諾依曼結構,成為了命令式語言的理論基礎。
因此當我們談及函數語言和命令式語言優劣的時候,我們實際上是在討論其背后的計算模型——也就是lambda演算和馮結構裝置操作——在執行效率和抽象層次上的優劣。
而面向對象技術則比較尷尬了,其背后沒有一個對應的計算模型(80年代的時候曾有人研究過,Pi演算是個備選,但是這個模型更多的是在并發對象領域的語義,而不是通常意義上的計算模型)。它有點類似于“最佳實踐”,在不同的計算模型上有著完全不同實現方式和含義。因此對比對象技術和其他技術的時候,搞清楚到底是哪一種面向對象就變得格外重要起來。
兩種不同的面向對象
目前流行的對象技術,實際上有兩個截然不同的源頭。它們分別在兩個完全不同的計算模型上發展起來,但是都頂著“面向對象”這個帽子。
第一種對象技術出現的較晚,在1979年以后。它是以抽象數據類型(ADT,Abstract Data Type)為源起,發展出來的面向對象技術。也就是首先被C++所采用的面向對象技術。
C++作為“更好的C”,繼承了C語言對于程序的看法,也就是數據抽象(Data Abstraction)和過程。面向對象技術在C++中,是作為一種更好的數據抽象的方式而存在的。
數據抽象在這類面向對象語言中是一種關鍵的抽象方式。所謂數據抽象,在計算機發展的早期是一種非常關鍵的技術。眾所周知,計算機在裝置模型上是一個存儲和一組指令集,而二進制的存儲實際上是沒有任何類型表示的。整數,浮點這些操作必須通過相應的約定,再以指令集的形式進行支持。而隨著計算機的發展,簡單的數據類型顯然已經不能滿足應用的需要。這時候一種靈活且有效的類型系統,就成了一種自然的追求(直到80年代初,類型系統都是計算機科學研究的重要方向之一)。
在C++中(以及后來的Java和C#),對象是一種構造數據類型的方式,把每個“類”看作一段存儲(狀態)和操作(方法)的集合。“類”作為已經存在的類型系統的一種擴展(這一點在C++中體現得尤其強烈)。在這類語言中,“類”(class)實際上代替了“對象”(object)成為了頭等公民。構造一個更好的類型系統,是這種面向對象技術所要解決的問題。與其說是面向對象,不如說是面向類或面向類型的。
從計算語義上說,這類對象技術仍然是裝置的操作語義,和面向過程的沒有實質上的區別。唯一的不同是,被這種對象語言操作的機器,可以借由對象技術擴展機器所支持的類型。這種面向對象技術是過程技術的一種發展,雖然在抽象層次上沒有什么太大的提高,但在實踐上已經是巨大的進步。
另一種對象技術出現的很早,大概在60年代末就出現了,直到80年代初還有發展。但是很長一段時間內并不是太主流的做法,反而并不太為人所知。
在函數式語言里,因為高階函數(High Order Function)的存在,數據可由函數來表達。這就是函數語言里一個非常重要的觀點:Data as Procedure。在函數語言中,可以構造一種非常類似于對象的高階函數:
(define (dispatch message)
(cond ((eq? message 'getName) name)
((eq? message 'getAge) age)
((eq? message 'getSex) sex))
(else (error 'messageNotUnderstand))))
dispatch)
(define vincent (make-user 'Vincent 30 'Male))
(vincent 'getName)
如上面的Lisp代碼所示,可以借由返回一個dispatch函數,將基本數據組合成一個更復雜的數據對象,而通過高階函數的后續調用,可以使用相應的選擇器(selector)與數據對象交互。這種風格的數據抽象被稱作“消息傳遞”(Message Passing),是早期面向對象技術的雛形,無論是Smalltalk還是CLOS都是以這種技術為藍本,設計的對象系統,包括后來的Ruby,實際上也是這種模型的一個發展。
因此實際上,就算在函數式語言上面,我們仍然可以通過引入這種對象的形式,對函數進行相應的模塊化和局部化。這種形式的對象與函數本身沒有任何差別,因此這種類型的對象系統,被稱作“方便的接口”,用于簡化對象的函數的訪問和調用。
在函數式語言里,另一個非常重要的概念就是“副作用”(Side effect,即函數可以修改某個存在的狀態)。像Lisp并不是純函數語言,因此是允許狀態修改的。因此對象技術除了可以被看作函數局部化和模塊化的方法之外,還可以看作副作用局部化的一種方式。采用這類面向對象技術的語言,通常被稱作動態面向對象語言。
這類對象語言通常都會保持一些函數式語言的特性,比如lambda的各種變體,比如較容易的函數組合,比如curry,比如高階函數。而且由于這類對象系統是從函數式發展出來的,也更加推崇一些副作用小的,利用高階函數的對象設計方法。比如,不變體(Immutable object)回調等等。
計算語義上,無副作用的對象系統實際上和Lambda演算享有同樣的計算語義。而帶副作用的本身只能被看作一種壞的實現,在函數上都沒有明確語義。僅僅能夠看作對于副作用的局部化和模塊化。
以上,我們簡單地看了一下兩種不同的“面向對象”技術。其中一種是用來解決如何構造更好的類型系統的,另一種是用來對函數和副作用進行有效模塊化和局部化的。如果單以這兩種面向對象技術和函數式語言去比較,實在不是一個層次的東西。那么為什么我們最近能夠聽到這么多函數和對象的討論呢?
新的發展
靜態類型函數語言
最早的函數語言是不太在意類型的,因為有Data as Procedure的存在,lambda演算可以通過把參數類型抽象成另一個高階函數來繞過函數參數類型問題(把參數也變成lambda,每個函數都看作參數和函數體的高階)。然而隨著形式化類型系統在理論上的發展,把lambda演算擴展為typed lambda演算自然就是一種很自然的推論。
隨著在此基礎上發展出來的ML族和Haskell語言的日漸成熟,以及代數數據類型(algebraic data type)的引入,這些語言可以較為容易地構造出非常復雜的類型系統。而且伴隨著類型推演和類型計算的引入,類型間復雜的關系也可以較為容易表達。由此,靜態類型函數式語言也開始挑戰以對象為基礎的類型系統構造方法。
實際上這里函數語言的挑戰是類型系統之爭,而非面向對象和函數語言之爭。因此,消息傳遞類的對象語言根本不在討論之列,而對于靜態類型面向對象語言而言,除了C++外(而對于C++,面向對象僅僅是構造類型系統的一種方式,另一種則是著名的范型編程。我仍然相信,在語義上靜態類型函數語言會勝過C++很多,但是彈性和表現力C++并不會差太多),其他主流語言如Java和C#,類型系統的已經被限制在一個相對簡單的范疇內,說完敗也不為過。
主流平臺也為需要處理復雜類型系統的開發者提供了不同的選擇,比如.NET平臺上的F#,以及JVM上的Scala,都是在主流平臺上引入靜態類型函數語言的一些特征,來簡化復雜類型系統的構造。
并發編程/并行計算/多核編程
Lisp并不是一個純函數語言,它允許有副作用存在。后來發展了一些嚴格的純函數語言,嚴格禁止副作用。也就是所有變量都和數學中的變量具有相同的語義,不能修改。然而計算機程序終歸是要處理狀態變化、輸入輸出這些不具有函數語義的操作的。一些純函數語言開始引入了更精巧的方式來管理狀態,比如Monad。Monad的傳遞性使得副作用的擴散在函數中變得更明確可見。
這種方式本來是用來解決純函數語言內副作用處理的一種技巧,但是恰好趕上Intel受制于生產技術,無法再通過提高單核頻率以追趕摩爾定律,必須通過集成多核的方式來制造更快的CPU。多核CPU作為一種新的事物,給計算機界帶來了新的恐慌,大家覺得有必要使用一種新的編程模型以充分利用多核的優勢。
而第一個嘗試的方案就是將計算分布到多個CPU上,也就是利用多核進行并行計算。這時候,純函數式語言對于副作用的處理,恰好給多核編譯器提供了一個理想的優化方式:即所有無作用的函數皆可以隨意分布到多核上,而帶副作用的函數則無法分布。通過對于類型系統的簡單識別和標注,就可以自動地將純函數式程序編譯為支持多核的程序。這在一段時間內,形成一種函數式語言是自動適應多核的,而面向對象程序則需要重寫的印象。一時間內,函數與對象之間的選擇實際上變成了多核和單核的選擇。
好在還有Amdahl's law存在,事實也證明除去一些特定的應用場景,自動編譯為支持多核并行的函數式程序并不快多少,而轉化為純函數程序的成本卻高出不少,同時大多數純函數語言都帶有學術性質,對于團隊開發并不友好。在加上JVM和.NET CLR對于多核都做出了一些回應。因此除去一些計算密集型應用,純函數語言并沒比面向對象好多少。
峰回路轉的是,由消息傳遞風格發展出來的actor模型,利用操作系統的進程/線程特性,在一個合理的粒度上很好地利用了多核的能力,簡化了并發編程。雖然第一個著名的實現是Erlang的actor系統,但是由于消息傳遞風格和面向對象模型相去不遠,很快就在各種面向對象語言中有了類庫支持。雖然利用當代函數語言的語法特性,actor可以實現得更簡潔,但是對象對于副作用和狀態的封裝,更好地解決了在并發環境下對于共享狀態的操作,反而有了更好的發展。
以上,我們看了函數式語言中兩個新的發展,以及圍繞這些發展涉及的一些“對象v.s.函數”的討論。正如本文一開始所說,對象技術作為一種工程實踐,其發展總是依托于其他更基本的計算模型的演化的。函數語言的發展,使得我們對于對象的認識和理解有了更深更好的認識。而對象作為函數的“方便的接口”總會在新的發展中,讓我們更加便利的享有函數式和其他計算模型發展的成果。
回到本文最開始的討論,函數的發展會的確會促使一些對象技術的消亡,但也會產生新的對象技術。或許更好的理解和掌握函數,類型系統才是真正掌握對象技術的捷徑,也未可知。
關于作者
徐昊,ThoughtWorks中國區首席技術專家,ThoughtWorks全球技術策略顧問(TAB),TW中國首席咨詢師。BJUG(Beijing Java User Group)和AgileChina創始人。從2003年起開始實踐極限編程等敏捷方法,2005年開始,多次以敏捷教練的角色幫助國內外多個團隊實施極限編程,Scrum和FDD等敏捷方法,敏捷交付和敏捷項目管理經驗極為豐富。目前主要致力于大規模團隊(300-500人)內的敏捷實踐和管理再造,以及對企業級技術應用趨勢和技術戰略的研究。