對象已死?

作者 徐昊 發佈於 2011510日 上午120

原文地址:http://www.infoq.com/cn/articles/object-have-dead

最近常有一種說法,就是我們如今面臨着另外一場編程模型的變革,面向對象技術已經處在被淘汰的邊緣,函數式語言會取代面向對象技術成爲主流方式,甚至出現了面向對象已死的言論。作爲一個硬核函數語言的狂熱者,我個人當然希望函數式語言可以一統天下,成爲主流之選。但是不是應該把對象技術和函數技術對立起來,說後者取前者而代之,我個人認爲,這和如何看待面向對象技術有關。

 

作爲工程實踐的對象技術

在這個年代,大家有一種神聖化面向對象技術的傾向,很多人都把對象技術奉爲高深的思想和理論。但實際上,面向對象技術僅僅一種工程實踐而已,它是依託於其他技術而存在的一種實踐,本身並不是一種完備的計算模型。

在計算機科學發展的早期,對於計算機的非數值計算應用的討論,以及對於可計算性問題的研究和發展,大抵確立了幾種的計算模型:遞歸函數類、圖靈機、Lambda演算、Horn子句、Post系統等等。其中,遞歸函數類是可計算性問題的數學解釋;Horn子句是prolog這類邏輯語言的理論基礎;lambda演算成爲了函數式語言的理論基礎;圖靈機是圖靈解決可計算問題的時候所設計的裝置,其後成爲計算機的裝置模型,與圖靈機相關的自動機以及馮諾依曼結構,成爲了命令式語言的理論基礎。

因此,當我們談及函數語言和命令式語言優劣的時候,我們實際上是在討論其背後的計算模型——也就是lambda演算和馮諾依曼結構裝置的操作——在執行效率和抽象層次上的優劣。

而面向對象技術則比較尷尬,其背後沒有一個對應的計算模型(80年代的時候曾有人研究過,Pi演算是個備選,但是這個模型更多的是在併發對象領域的語義,而不是通常意義上的計算模型)。它有點類似於“最佳實踐”,在不同的計算模型上有着完全不同實現方式和含義。因此對比對象技術和其他技術的時候,搞清楚到底是哪一種面向對象就變得格外重要起來。

 

兩種不同的面向對象

目前流行的對象技術,實際上有兩個截然不同的源頭。它們分別在兩個完全不同的計算模型上發展起來,但都頂着“面向對象”這個帽子。

第一種對象技術出現的較晚,在1979年以後。它是以抽象數據類型(ADTAbstractData Type)爲源起,發展出來的面向對象技術。也就是首先被C++所採用的面向對象技術。

C++作爲“更好的C”,繼承了C語言對於程序的看法,也就是數據抽象(Data Abstraction)和過程。面向對象技術在C++中,是作爲一種更好的數據抽象的方式而存在的。

數據抽象在這類面嚮對象語言中是一種關鍵的抽象方式。所謂數據抽象,在計算機發展的早期是一種非常關鍵的技術。衆所周知,計算機在裝置模型上是一個存儲和一組指令集,而二進制的存儲實際上是沒有任何類型表示的。整數,浮點這些操作必須通過相應的約定,再以指令集的形式進行支持。而隨着計算機的發展,簡單的數據類型顯然已經不能滿足應用的需要。這時候一種靈活且有效的類型系統,就成了一種自然的追求(直到80年代初,類型系統都是計算機科學研究的重要方向之一)。

C++中(以及後來的JavaC#),對象是一種構造數據類型的方式,把每個“類”看作一段存儲(狀態)和操作(方法)的集合。“類”作爲已經存在的類型系統的一種擴展(這一點在C++中體現得尤其強烈)。在這類語言中,“類”(class)實際上代替了“對象”(object)成爲了頭等公民。構造一個更好的類型系統,是這種面向對象技術所要解決的問題。與其說是面向對象,不如說是面向類或面向類型的。

從計算語義上說,這類對象技術仍然是裝置的操作語義,和麪向過程的沒有實質上的區別。唯一的不同是,被這種對象語言操作的機器,可以藉由對象技術擴展機器所支持的類型。這種面向對象技術是過程技術的一種發展,雖然在抽象層次上沒有什麼太大的提高,但在實踐上已經是巨大的進步。

另一種對象技術出現的很早,大概在60年代末,直到80年代初還有發展。但是很長一段時間內並不是太主流的做法,反而並不太爲人所知。

在函數式語言裏,因爲高階函數(High Order Function)的存在,數據可由函數來表達。這就是函數語言裏一個非常重要的觀點:Data as Procedure。在函數語言中,可以構造一種非常類似於對象的高階函數:

(define (make-username age sex)
  (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++並不會差太多),其他主流語言如JavaC#,類型系統的已經被限制在一個相對簡單的範疇內,說完敗也不爲過。

主流平臺也爲需要處理複雜類型系統的開發者提供了不同的選擇,比如.NET平臺上的F#。以及JVM上的Scala。都是在主流平臺上引入靜態類型函數語言的一些特徵,來簡化複雜類型系統的構造。

 

併發編程/並行計算/多核編程

Lisp並不是一個純函數語言,它允許有副作用存在。後來發展了一些嚴格的純函數語言,嚴格禁止副作用。也就是所有變量都和數學中的變量具有相同的語義,不能修改。然而計算機程序終歸是要處理狀態變化、輸入輸出這些不具有函數語義的操作的。一些純函數語言開始引入了更精巧的方式來管理狀態,比如MonadMonad的傳遞性使得副作用的擴散在函數中變得更明確可見。

這種方式本來是用來解決純函數語言內副作用處理的一種技巧,但是恰好趕上Intel受制於生產技術,無法再通過提高單核頻率以追趕摩爾定律,必須通過集成多核的方式來製造更快的CPU。多核CPU作爲一種新的事物,給計算機界帶來了新的恐慌,大家覺得有必要使用一種新的編程模型以充分利用多核的優勢。

而第一個嘗試的方案就是將計算分佈到多個CPU上,也就是利用多核進行並行計算。這時,純函數式語言對於副作用的處理,恰好給多核編譯器提供了一個理想的優化方式:即所有無作用的函數皆可以隨意分佈到多核上,而帶副作用的函數則無法分佈。通過對類型系統的簡單識別和標註,就可以自動地將純函數式程序編譯爲支持多核的程序。這在一段時間內,形成一種函數式語言是自動適應多核的,而面向對象程序則需要重寫的印象。一時間內,函數與對象之間的選擇實際上變成了多核和單核的選擇。

好在還有Amdahl's Law存在,事實也證明除去一些特定的應用場景,自動編譯爲支持多核並行的函數式程序並不快多少,而轉化爲純函數程序的成本卻高出不少,同時大多數純函數語言都帶有學術性質,對於團隊開發並不友好。在加上JVM.NET CLR對於多核都做出了一些迴應。因此除去一些計算密集型應用,純函數語言並沒比面向對象好多少。

峯迴路轉的是,由消息傳遞風格發展出來的actor模型,利用操作系統的進程/線程特性,在一個合理的粒度上很好地利用了多核的能力,簡化了併發編程。雖然第一個著名的實現是Erlangactor系統,但是由於消息傳遞風格和麪向對象模型相去不遠,很快就在各種面嚮對象語言中有了類庫支持。雖然利用當代函數語言的語法特性,actor可以實現得更簡潔,但是對象對於副作用和狀態的封裝,更好地解決了在併發環境下對於共享狀態的操作,反而有了更好的發展。

以上,我們看了函數式語言中兩個新的發展,以及圍繞這些發展涉及的一些“對象v.s.函數”的討論。正如本文一開始所說,對象技術作爲一種工程實踐,其發展總是依託於其他更基本的計算模型的演化。函數語言的發展,使得我們對於對象的認識和理解有了更深更好的認識。而對象作爲函數的“方便的接口”總會在新的發展中,讓我們更加便利的享有函數式和其他計算模型發展的成果。

回到本文最開始的討論,函數式的發展的確會促使一些對象技術的消亡,但也會產生新的對象技術。或許更好的理解和掌握函數,類型系統纔是真正掌握對象技術的捷徑,也未可知。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章