對象已死?
最近常有一種說法,就是我們?nèi)缃衩媾R著另外一場編程模型的變革,面向?qū)ο蠹夹g(shù)已經(jīng)處在被淘汰的邊緣,函數(shù)式語言會取代面向?qū)ο蠹夹g(shù)成為主流方式,甚至出現(xiàn)了面向?qū)ο笠阉赖难哉摗W鳛橐粋€硬核函數(shù)語言的狂熱者,我個人當(dāng)然希望函數(shù)式語言可以一統(tǒng)天下,成為主流之選。但是不是應(yīng)該把對象技術(shù)和函數(shù)技術(shù)對立起來,說式后者取前者而代之,我個人認(rèn)為,這和如何看待面向?qū)ο蠹夹g(shù)有關(guān)。
做為工程實踐的對象技術(shù)
在這個年代,大家有一種神圣化面向?qū)ο蠹夹g(shù)的傾向,很多人都把對象技術(shù)奉為高深的思想和理論。但實際上,面向?qū)ο蠹夹g(shù)僅僅一種工程實踐而已,它是依托于其他技術(shù)而存在的一種實踐,本身并不是一種完備的計算模型。
在計算機科學(xué)發(fā)展的早期,對于計算機的非數(shù)值計算應(yīng)用的討論,以及對于可計算性問題的研究和發(fā)展,大抵確立了幾種的計算模型:遞歸函數(shù)類、圖靈機、Lambda演算、Horn子句、Post系統(tǒng)等等。其中遞歸函數(shù)類是可計算性問題的數(shù)學(xué)解釋;Horn子句是prolog這類邏輯語言的理論基礎(chǔ);lambda演算成為了函數(shù)式語言的理論基礎(chǔ);圖靈機是圖靈解決可計算問題的時候所設(shè)計的裝置,其后成為計算機的裝置模型,與圖靈機相關(guān)的自動機以及馮諾依曼結(jié)構(gòu),成為了命令式語言的理論基礎(chǔ)。
因此當(dāng)我們談及函數(shù)語言和命令式語言優(yōu)劣的時候,我們實際上是在討論其背后的計算模型——也就是lambda演算和馮結(jié)構(gòu)裝置操作——在執(zhí)行效率和抽象層次上的優(yōu)劣。
而面向?qū)ο蠹夹g(shù)則比較尷尬了,其背后沒有一個對應(yīng)的計算模型(80年代的時候曾有人研究過,Pi演算是個備選,但是這個模型更多的是在并發(fā)對象領(lǐng)域的語義,而不是通常意義上的計算模型)。它有點類似于“***實踐”,在不同的計算模型上有著完全不同實現(xiàn)方式和含義。因此對比對象技術(shù)和其他技術(shù)的時候,搞清楚到底是哪一種面向?qū)ο缶妥兊酶裢庵匾饋怼?/p>
兩種不同的面向?qū)ο?/strong>
目前流行的對象技術(shù),實際上有兩個截然不同的源頭。它們分別在兩個完全不同的計算模型上發(fā)展起來,但是都頂著“面向?qū)ο?rdquo;這個帽子。
***種對象技術(shù)出現(xiàn)的較晚,在1979年以后。它是以抽象數(shù)據(jù)類型(ADT,Abstract Data Type)為源起,發(fā)展出來的面向?qū)ο蠹夹g(shù)。也就是首先被C++所采用的面向?qū)ο蠹夹g(shù)。
C++作為“更好的C”,繼承了C語言對于程序的看法,也就是數(shù)據(jù)抽象(Data Abstraction)和過程。面向?qū)ο蠹夹g(shù)在C++中,是作為一種更好的數(shù)據(jù)抽象的方式而存在的。
數(shù)據(jù)抽象在這類面向?qū)ο笳Z言中是一種關(guān)鍵的抽象方式。所謂數(shù)據(jù)抽象,在計算機發(fā)展的早期是一種非常關(guān)鍵的技術(shù)。眾所周知,計算機在裝置模型上是一個存儲和一組指令集,而二進(jìn)制的存儲實際上是沒有任何類型表示的。整數(shù),浮點這些操作必須通過相應(yīng)的約定,再以指令集的形式進(jìn)行支持。而隨著計算機的發(fā)展,簡單的數(shù)據(jù)類型顯然已經(jīng)不能滿足應(yīng)用的需要。這時候一種靈活且有效的類型系統(tǒng),就成了一種自然的追求(直到80年代初,類型系統(tǒng)都是計算機科學(xué)研究的重要方向之一)。
在C++中(以及后來的Java和C#),對象是一種構(gòu)造數(shù)據(jù)類型的方式,把每個“類”看作一段存儲(狀態(tài))和操作(方法)的集合。“類”作為已經(jīng)存在的類型系統(tǒng)的一種擴展(這一點在C++中體現(xiàn)得尤其強烈)。在這類語言種,“類”(class)實際上代替了“對象”(object)成為了頭等公民。構(gòu)造一個更好的類型系統(tǒng),是這種面向?qū)ο蠹夹g(shù)所要解決的問題。與其說是面向?qū)ο?,不如說是面向類或面向類型的。
從計算語義上說,這類對象技術(shù)仍然是裝置的操作語義,和面向過程的沒有實質(zhì)上的區(qū)別。唯一的不同是,被這種對象語言操作的機器,可以借由對象技術(shù)擴展機器所支持的類型。這種面向?qū)ο蠹夹g(shù)是過程技術(shù)的一種發(fā)展,雖然在抽象層次上沒有什么太大的提高,但在實踐上已經(jīng)是巨大的進(jìn)步。
另一種對象技術(shù)出現(xiàn)的很早,大概在60年代末就出現(xiàn)了,直到80年代初還有發(fā)展。但是很長一段時間內(nèi)并不是太主流的做法,反而并不太為人所知。
在函數(shù)式語言里,因為高階函數(shù)(High Order Function)的存在,數(shù)據(jù)可由函數(shù)來表達(dá)。這就是函數(shù)語言里一個非常重要的觀點:Data as Procedure。在函數(shù)語言中,可以構(gòu)造一種非常類似于對象的高階函數(shù):
- (define (make-user name 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函數(shù),將基本數(shù)據(jù)組合成一個更復(fù)雜的數(shù)據(jù)對象,而通過高階函數(shù)的后續(xù)調(diào)用,可以使用相應(yīng)的選擇器(selector)與數(shù)據(jù)對象交互。這種風(fēng)格的數(shù)據(jù)抽象被稱作“消息傳遞”(Message Passing),是早期面向?qū)ο蠹夹g(shù)的雛形,無論是Smalltalk還是CLOS都是以這種技術(shù)為藍(lán)本,設(shè)計的對象系統(tǒng),包括后來的Ruby,實際上也是這種模型的一個發(fā)展。
因此實際上,就算在函數(shù)式語言上面,我們?nèi)匀豢梢酝ㄟ^引入這種對象的形式,對函數(shù)進(jìn)行相應(yīng)的模塊化和局部化。這種形式的對象與函數(shù)本身沒有任何差別,因此這種類型的對象系統(tǒng),被稱作“方便的接口”,用于簡化對象的函數(shù)的訪問和調(diào)用。
在函數(shù)式語言里,另一個非常重要的概念就是“副作用”(Side effect,即函數(shù)可以修改某個存在的狀態(tài))。像Lisp并不是純函數(shù)語言,因此是允許狀態(tài)修改的。因此對象技術(shù)除了可以被看作函數(shù)局部化和模塊化的方法之外,還可以看作副作用局部化的一種方式。采用這類面向?qū)ο蠹夹g(shù)的語言,通常被稱作動態(tài)面向?qū)ο笳Z言。
這類對象語言通常都會保持一些函數(shù)式語言的特性,比如lambda的各種變體,比如較容易的函數(shù)組合,比如curry,比如高階函數(shù)。而且由于這類對象系統(tǒng)是從函數(shù)式發(fā)展出來的,也更加推崇一些副作用小的,利用高階函數(shù)的對象設(shè)計方法。比如,不變體(Immutable object)回調(diào)等等。
計算語義上,無副作用的對象系統(tǒng)實際上和Lambda演算享有同樣的計算語義。而帶副作用的本身只能被看作一種壞的實現(xiàn),在函數(shù)上都沒有明確語義。僅僅能夠看作對于副作用的局部化和模塊化。
以上,我們簡單地看了一下兩種不同的“面向?qū)ο?rdquo;技術(shù)。其中一種是用來解決如何構(gòu)造更好的類型系統(tǒng)的,另一種是用來對函數(shù)和副作用進(jìn)行有效模塊化和局部化的。如果單以這兩種面向?qū)ο蠹夹g(shù)和函數(shù)式語言去比較,實在不是一個層次的東西。那么為什么我們最近能夠聽到這么多函數(shù)和對象的討論呢?
新的發(fā)展
靜態(tài)類型函數(shù)語言
最早的函數(shù)語言是不太在意類型的,因為有Data as Procedure的存在,lambda演算可以通過把參數(shù)類型抽象成另一個高階函數(shù)來繞過函數(shù)參數(shù)類型問題(把參數(shù)也變成lambda,每個函數(shù)都看作參數(shù)和函數(shù)體的高階)。然而隨著形式化類型系統(tǒng)在理論上的發(fā)展,把lambda演算擴展為typed lambda演算自然就是一種很自然的推論。
隨著在此基礎(chǔ)上發(fā)展出來的ML族和Haskell語言的日漸成熟,以及代數(shù)數(shù)據(jù)類型(algebraic data type)的引入,這些語言可以較為容易地構(gòu)造出非常復(fù)雜的類型系統(tǒng)。而且伴隨著類型推演和類型計算的引入,類型間復(fù)雜的關(guān)系也可以較為容易表達(dá)。由此,靜態(tài)類型函數(shù)式語言也開始挑戰(zhàn)以對象為基礎(chǔ)的類型系統(tǒng)構(gòu)造方法。
實際上這里函數(shù)語言的挑戰(zhàn)是類型系統(tǒng)之爭,而非面向?qū)ο蠛秃瘮?shù)語言之爭。因此,消息傳遞類的對象語言根本不在討論之列,而對于靜態(tài)類型面向?qū)ο笳Z言而言,除了C++外(而對于C++,面向?qū)ο髢H僅是構(gòu)造類型系統(tǒng)的一種方式,另一種則是著名的范型編程。我仍然相信,在語義上靜態(tài)類型函數(shù)語言會勝過C++很多,但是彈性和表現(xiàn)力C++并不會差太多),其他主流語言如Java和C#,類型系統(tǒng)的已經(jīng)被限制在一個相對簡單的范疇內(nèi),說完敗也不為過。
主流平臺也為需要處理復(fù)雜類型系統(tǒng)的開發(fā)者提供了不同的選擇,比如.NET平臺上的F#。以及JVM上的Scala。都是在主流平臺上引入靜態(tài)類型函數(shù)語言的一些特征,來簡化復(fù)雜類型系統(tǒng)的構(gòu)造。
并發(fā)編程/并行計算/多核編程
Lisp并不是一個純函數(shù)語言,它允許有副作用存在。后來發(fā)展了一些嚴(yán)格的純函數(shù)語言,嚴(yán)格禁止副作用。也就是所有變量都和數(shù)學(xué)中的變量具有相同的語義,不能修改。然而計算機程序終歸是要處理狀態(tài)變化、輸入輸出這些不具有函數(shù)語義的操作的。一些純函數(shù)語言開始引入了更精巧的方式來管理狀態(tài),比如Monad。Monad的傳遞性使得副作用的擴散在函數(shù)中變得更明確可見。
這種方式本來是用來解決純函數(shù)語言內(nèi)副作用處理的一種技巧,但是恰好趕上Intel受制于生產(chǎn)技術(shù),無法再通過提高單核頻率以追趕摩爾定律,必須通過集成多核的方式來制造更快的CPU。多核CPU作為一種新的事物,給計算機界帶來了新的恐慌,大家覺得有必要使用一種新的編程模型以充分利用多核的優(yōu)勢。
而***個嘗試的方案就是將計算分布到多個CPU上,也就是利用多核進(jìn)行并行計算。這時候,純函數(shù)式語言對于副作用的處理,恰好給多核編譯器提供了一個理想的優(yōu)化方式:即所有無作用的函數(shù)皆可以隨意分布到多核上,而帶副作用的函數(shù)則無法分布。通過對于類型系統(tǒng)的簡單識別和標(biāo)注,就可以自動地將純函數(shù)式程序編譯為支持多核的程序。這在一段時間內(nèi),形成一種函數(shù)式語言是自動適應(yīng)多核的,而面向?qū)ο蟪绦騽t需要重寫的印象。一時間內(nèi),函數(shù)與對象之間的選擇實際上變成了多核和單核的選擇。
好在還有Amdahl's law存在,事實也證明除去一些特定的應(yīng)用場景,自動編譯為支持多核并行的函數(shù)式程序并不快多少,而轉(zhuǎn)化為純函數(shù)程序的成本卻高出不少,同時大多數(shù)純函數(shù)語言都帶有學(xué)術(shù)性質(zhì),對于團隊開發(fā)并不友好。在加上JVM和.NET CLR對于多核都做出了一些回應(yīng)。因此除去一些計算密集型應(yīng)用,純函數(shù)語言并沒比面向?qū)ο蠛枚嗌佟?/p>
峰回路轉(zhuǎn)的是,由消息傳遞風(fēng)格發(fā)展出來的actor模型,利用操作系統(tǒng)的進(jìn)程/線程特性,在一個合理的粒度上很好地利用了多核的能力,簡化了并發(fā)編程。雖然***個著名的實現(xiàn)是Erlang的actor系統(tǒng),但是由于消息傳遞風(fēng)格和面向?qū)ο竽P拖嗳ゲ贿h(yuǎn),很快就在各種面向?qū)ο笳Z言中有了類庫支持。雖然利用當(dāng)代函數(shù)語言的語法特性,actor可以實現(xiàn)得更簡潔,但是對象對于副作用和狀態(tài)的封裝,更好地解決了在并發(fā)環(huán)境下對于共享狀態(tài)的操作,反而有了更好的發(fā)展。
以上,我們看了函數(shù)式語言中兩個新的發(fā)展,以及圍繞這些發(fā)展涉及的一些“對象v.s.函數(shù)”的討論。正如本文一開始所說,對象技術(shù)作為一種工程實踐,其發(fā)展總是依托于其他更基本的計算模型的演化的。函數(shù)語言的發(fā)展,使得我們對于對象的認(rèn)識和理解有了更深更好的認(rèn)識。而對象作為函數(shù)的“方便的接口”總會在新的發(fā)展中,讓我們更加便利的享有函數(shù)式和其他計算模型發(fā)展的成果。
回到本文最開始的討論,函數(shù)的發(fā)展會的確會促使一些對象技術(shù)的消亡,但也會產(chǎn)生新的對象技術(shù)?;蛟S更好的理解和掌握函數(shù),類型系統(tǒng)才是真正掌握對象技術(shù)的捷徑,也未可知。
本文通過對對象技術(shù)和函數(shù)技術(shù)的介紹,闡述了作者的觀點,其實并不存在,誰會取代誰的說法,這只是如何看待面向?qū)ο蠹夹g(shù)的問題。希望對你會有幫助。
【編輯推薦】