哪種語(yǔ)言將統(tǒng)治多核時(shí)代 再看函數(shù)式語(yǔ)言特性
譯文51CTO編輯推薦:Scala編程語(yǔ)言專題
【51CTO外電精選】最近這幾年,軟件開(kāi)發(fā)語(yǔ)言可謂是層出不窮。在這些新的編程語(yǔ)言中,最多的就是函數(shù)式語(yǔ)言。本文將向你介紹函數(shù)式語(yǔ)言的概念、術(shù)語(yǔ)、方法以及幾種典型的函數(shù)式語(yǔ)言。本文面向的讀者是那些已經(jīng)懂得其它編程語(yǔ)言、但卻對(duì)函數(shù)式語(yǔ)言沒(méi)有了解的開(kāi)發(fā)人員。
什么是函數(shù)式語(yǔ)言?
如果你已經(jīng)用面向?qū)ο蟮恼Z(yǔ)言(例如Java和C#)寫了很長(zhǎng)時(shí)間的代碼,那么可能很難想象出另一種新的編程思維方式,而函數(shù)式語(yǔ)言恰恰做到了這一點(diǎn)。它的核心就是通過(guò)對(duì)算法進(jìn)行功能分解,從而解決軟件問(wèn)題。在函數(shù)式語(yǔ)言里,函數(shù)是首要的。如果你是Java陣營(yíng)出身,那應(yīng)該能理解到這之間的差異。在Java中,實(shí)現(xiàn)某種方法的唯一形式就是將其作為某個(gè)類的成員。
雖然最近風(fēng)頭正勁的某些特殊語(yǔ)言引人注目,但是函數(shù)式語(yǔ)言其實(shí)也可以代表一門技術(shù),而不僅僅是一種語(yǔ)言。我們可以用函數(shù)式編程的方式,用面向?qū)ο蟮木幊陶Z(yǔ)言實(shí)現(xiàn)一般的功能(后面的章節(jié)里我們就會(huì)看到一個(gè)用函數(shù)式方法編寫的C#程序)。雖然這可以實(shí)現(xiàn),但是在稍大一點(diǎn)的程序中,我們很快就會(huì)感覺(jué)到缺乏表現(xiàn)力,反模式(anti-pattern)也隨之出現(xiàn)。試想一下,不用extend和implement關(guān)鍵字寫個(gè)上規(guī)模的Java程序有多痛苦。由于這些困難的存在,人們需要一種新的語(yǔ)言:一種函數(shù)式語(yǔ)言。
為何需要函數(shù)式語(yǔ)言?
很明顯,現(xiàn)代計(jì)算平臺(tái)上發(fā)生了一個(gè)重大變化,那就是多核技術(shù)的引入。除了上網(wǎng)本和PDA,我們甚至都找不出還采用單核處理器的臺(tái)式機(jī)和筆記本電腦了。我們正在向多核心,多處理器發(fā)展,并且所有跡象都表明這一趨勢(shì)將繼續(xù)下去。除了采用多核心之外,高運(yùn)算量和高復(fù)雜度的算法應(yīng)用都傾向于優(yōu)化使用圖形處理單元(GPU),從而提高并行性。歸納起來(lái),從開(kāi)發(fā)者的角度來(lái)說(shuō),這些都屬于并發(fā)問(wèn)題的范疇。
我們大多數(shù)的編程語(yǔ)言都不容易實(shí)現(xiàn)并發(fā)。想想幾十年前,C語(yǔ)言程序里的錯(cuò)誤處理直接就被代碼基底(code base)丟棄了。它與業(yè)務(wù)邏輯混在了一起。C函數(shù)成功后就將返回0 ,失敗則返回錯(cuò)誤代碼。很明顯這不是很理想的辦法,但是C語(yǔ)言本身的表達(dá)能力限制了開(kāi)發(fā)者們用其它方法來(lái)進(jìn)行出錯(cuò)處理。其他語(yǔ)言對(duì)此進(jìn)行了改進(jìn),在C++或者Java中,出錯(cuò)時(shí)會(huì)拋出異常,異常處理程序把出錯(cuò)處理和正常事務(wù)處理分離開(kāi)來(lái)。有些人可能會(huì)說(shuō)這也算不上什么太好的辦法,但是這至少是個(gè)不小的進(jìn)步了。在解決并發(fā)這個(gè)問(wèn)題上,我們所掌握的技術(shù)的成熟度也和這差不多。如果想要在一個(gè)用面向?qū)ο笳Z(yǔ)言編程的程序里實(shí)現(xiàn)并行,那編寫起來(lái)真得費(fèi)一番腦筋。像生成一個(gè)打印任務(wù)線程并不需要處理多少并發(fā)控制,但是往往它還會(huì)牽扯到進(jìn)程間的狀態(tài)共享、顯示器的阻塞。隨著內(nèi)核數(shù)的增加,同時(shí)運(yùn)行的線程數(shù)可能進(jìn)一步增大,系統(tǒng)的效率也將隨之降低。這時(shí),我們就需要一種新的語(yǔ)言,讓我們能從這些細(xì)節(jié)工作中抽身出來(lái),以更好地利用并發(fā)。
函數(shù)式語(yǔ)言已經(jīng)在簡(jiǎn)化并行開(kāi)發(fā)中證明了它的作用, 這得益于它既不用共享內(nèi)存,也不會(huì)產(chǎn)生副作用(side effect)的函數(shù)。進(jìn)一步深入函數(shù)式語(yǔ)言,你就會(huì)發(fā)現(xiàn)它讓開(kāi)發(fā)者從并發(fā)這個(gè)概念中抽身出來(lái)了,讓開(kāi)發(fā)者不用老是想著現(xiàn)在CPU是在并發(fā)作業(yè)。許多語(yǔ)言實(shí)現(xiàn)了一種并發(fā)開(kāi)發(fā)模式,通常稱之為Actor模型。在這種模型下,進(jìn)程間傳遞消息,而不是共享狀態(tài),從而消除線程阻塞。
函數(shù)式語(yǔ)言的另一大寶貴優(yōu)點(diǎn)就是簡(jiǎn)潔。在Stuart Halloway的《Programming Clojure》一書第一章中,Stuart展示了3行clojure代碼,這比用Jakarta Commons框架開(kāi)發(fā)減小了三分之一的代碼量,同時(shí)還體現(xiàn)出更清晰明了的邏輯思路。
有一個(gè)很重要的觀點(diǎn)就是,函數(shù)式語(yǔ)言不是用來(lái)取代面向過(guò)程或者面向?qū)ο蟮木幊陶Z(yǔ)言的??纯次覀?cè)谏弦还?jié)中所列舉出來(lái)的幾種函數(shù)式語(yǔ)言,就會(huì)發(fā)現(xiàn)許多新的函數(shù)式語(yǔ)言都是多范型(multi-paradigm),很多時(shí)候這些語(yǔ)言都是運(yùn)行在虛擬機(jī)上,并且作為其它面向?qū)ο笳Z(yǔ)言和命令式語(yǔ)言的橋梁出現(xiàn)的。選擇適合手頭工作的語(yǔ)言才是最重要的。我希望從業(yè)者們?cè)陂_(kāi)發(fā)主流應(yīng)用時(shí)繼續(xù)使用Java,Groovy或者C#這些通用語(yǔ)言,但當(dāng)面臨著一個(gè)極為復(fù)雜的算法或需要實(shí)現(xiàn)高并發(fā)時(shí),最好還是轉(zhuǎn)而用函數(shù)式語(yǔ)言來(lái)集成這些方案。這也正是Neal Ford說(shuō)了多年的 “多語(yǔ)言程序員”(polyglot programmer)。對(duì)于此類程序員的形成,我們可以參考一下一位Java兼Scala開(kāi)發(fā)者的學(xué)習(xí)歷程。
幾種典型的函數(shù)式語(yǔ)言
當(dāng)我們回顧歷史上的編程語(yǔ)言時(shí),就會(huì)發(fā)現(xiàn)其實(shí)函數(shù)式語(yǔ)言并不是一個(gè)新生事物,它早就出現(xiàn)過(guò)。其中最廣為人知的幾種“祖父”級(jí)語(yǔ)言包括:LISP和FORTRAN 。自1980年代中期以來(lái),這些語(yǔ)言在企業(yè)和商業(yè)開(kāi)發(fā)領(lǐng)域逐漸讓位于面向?qū)ο蟮拈_(kāi)發(fā)語(yǔ)言,流行領(lǐng)域也逐漸縮小到只剩下學(xué)術(shù)界。不過(guò)下面列出的這幾種函數(shù)式語(yǔ)言最近正在向商業(yè)領(lǐng)域發(fā)起反攻:
◆Erlang:這是一種以A.K Erlang的名字命名的通用并行編程語(yǔ)言。它有函數(shù)式語(yǔ)言的元素,以及一個(gè)Actor 并發(fā)模型,從而簡(jiǎn)化并行開(kāi)發(fā)工作。編輯推薦對(duì)Erlang感興趣的讀者閱讀一下51CTO以前的一次訪談:因并發(fā)而生 因云計(jì)算而熱:Erlang專家訪談實(shí)錄。
◆Haskell:這是一門已經(jīng)有超過(guò)20年歷史的開(kāi)源編程語(yǔ)言,它的設(shè)計(jì)宗旨就是成為一門純粹的函數(shù)式語(yǔ)言。
◆OCaml:面向?qū)ο蟮腃aml(Objective Caml)是Caml語(yǔ)言的一個(gè)開(kāi)源版本,Caml語(yǔ)言可以算是ML語(yǔ)言的一個(gè)方言版了,ML語(yǔ)言1970年就已經(jīng)開(kāi)發(fā)出來(lái)了,也是作為一種通用函數(shù)式語(yǔ)言存在的。它被認(rèn)為是后來(lái)出現(xiàn)的F#等多種函數(shù)式語(yǔ)言的基礎(chǔ)。
◆Lisp:表處理語(yǔ)言(List Processing Language)是一種函數(shù)式語(yǔ)言,最初是于1958年擬定的。由它派生出了許多分支。
◆Scala:Scala 語(yǔ)言的設(shè)計(jì)目標(biāo)是在Java虛擬機(jī)上實(shí)現(xiàn)函數(shù)式和面向?qū)ο筮@兩類編程語(yǔ)言的集成。它是一種強(qiáng)類型的編程語(yǔ)言。Scala編程語(yǔ)言近年來(lái)的流行度在不斷提升,編輯推薦讀者參閱51CTO的Scala編程語(yǔ)言專題。
◆Clojure:Clojure是Lisp語(yǔ)言的一個(gè)現(xiàn)代分支,它運(yùn)行在Java虛擬機(jī)上,是為并發(fā)程序開(kāi)發(fā)設(shè)計(jì)的。它是一種動(dòng)態(tài)類型編程語(yǔ)言。
◆F#:這是一種運(yùn)行在.Net CLR平臺(tái)上的新語(yǔ)言。它是OCaml的一個(gè)分支,它兼具了函數(shù)式和命令式面向?qū)ο笳Z(yǔ)言的特點(diǎn)。同時(shí)它也是一種強(qiáng)類型的編程語(yǔ)言。F#在未來(lái)的.NET平臺(tái)上有重要的作用,將在Visual Studio 2010中被正式包含。
值得注意的是函數(shù)式語(yǔ)言并不一定要是動(dòng)態(tài)語(yǔ)言(dynamic language)。函數(shù)式語(yǔ)言允許動(dòng)態(tài)或靜態(tài)類型。這里所列出的語(yǔ)言只是各種各樣函數(shù)式語(yǔ)言中的一個(gè)子集,每一種實(shí)現(xiàn)了某種特定的需求。本文將介紹好幾種典型函數(shù)式語(yǔ)言,而不是專門講解某一種語(yǔ)言。另外我們還有一個(gè)沒(méi)有回答的問(wèn)題就是:為什么現(xiàn)在對(duì)函數(shù)式語(yǔ)言的需求越來(lái)越強(qiáng)烈?
#p#
函數(shù)式語(yǔ)言的特色
函數(shù)式語(yǔ)言的根本宗旨之一就是它不是一種命令式(imperative)語(yǔ)言。在命令式語(yǔ)言中,函數(shù)中定義的變量通常都代表內(nèi)存中的一塊特定大小的區(qū)域,而且賦給它的值也通常允許在整個(gè)方法中改變。而在函數(shù)式語(yǔ)言中,對(duì)變量的賦值是綁定性的,就像在數(shù)學(xué)函數(shù)式中那樣。比如說(shuō),有這樣的數(shù)學(xué)式:let x=2。這就是說(shuō)對(duì)于這個(gè)問(wèn)題,x的值為2。x的值不能改變,總是2。按照這樣的模式,我們平常編程過(guò)程中的一些寫法就沒(méi)有意義了,例如這個(gè)賦值語(yǔ)句:let x=x+1。在命令式語(yǔ)言中這是有意義的,但是在數(shù)學(xué)上,這是沒(méi)有任何意義的,因?yàn)閤=x+1是無(wú)解的。理解這個(gè)概念后,函數(shù)式開(kāi)發(fā)(functional development)就算是上路了。
和在數(shù)學(xué)中一樣,函數(shù)式語(yǔ)言中的賦值并不僅限于數(shù)值型。方法在函數(shù)式語(yǔ)言中是最為重要的。因此,一個(gè)方法閉包(method closure)可以用來(lái)給變量賦值,并且被傳遞或調(diào)整到其它的函數(shù)表達(dá)式里。用數(shù)學(xué)表達(dá)式來(lái)說(shuō)就相當(dāng)于:let x=f(y)。數(shù)學(xué)上稱之為:x是以y為自變量的函數(shù)f 的函數(shù)值。任給一個(gè)y值,都有一個(gè)對(duì)應(yīng)的x值。這就是函數(shù)式編程的另一個(gè)核心思想。只要y不變,那么x也總是一個(gè)特定的對(duì)應(yīng)值,不會(huì)發(fā)生改變。
雖然不同的函數(shù)式語(yǔ)言之間有一些不同之處,但是他們都有以下一些共同點(diǎn):
◆函數(shù)閉包支持
◆高階函數(shù)
◆用for流程來(lái)實(shí)現(xiàn)遞歸
◆沒(méi)有副作用(side-effects)
◆把重點(diǎn)放在“要計(jì)算什么”,而不是“如何去計(jì)算”上。
◆引用透明性(Referential transparency)
函數(shù)式語(yǔ)言的功能和術(shù)語(yǔ)
伴隨著函數(shù)式語(yǔ)言的發(fā)展,涌現(xiàn)了許多新的術(shù)語(yǔ),但是沒(méi)有哪種能比Lambda產(chǎn)生得更快。就像我們前面提到的一樣,函數(shù)式編程和數(shù)學(xué)界有很大的聯(lián)系。Lambda指的是λ演算(lambda calculus 或 l-calculus)λ演算是一套用于研究函數(shù)定義、函數(shù)應(yīng)用和函數(shù)遞歸的系統(tǒng)。
清單1 :一個(gè)簡(jiǎn)單的Lambda表達(dá)式
還有許多的λ表達(dá)式我們這里都不再深入討論了。如清單1所示的幾個(gè)簡(jiǎn)單表達(dá)式,λ提供了一種全新的語(yǔ)法。上面所舉的例子表示的是一個(gè)一元函數(shù),這意味著函數(shù)只需要一個(gè)參數(shù),或者說(shuō)元數(shù)是1.在清單2中,我們可以看到把一個(gè)函數(shù)作為另一個(gè)函數(shù)的參數(shù)。
清單2 :一個(gè)簡(jiǎn)單的λ函數(shù)作參數(shù)傳遞
λ表達(dá)式在線指引上對(duì)這些有詳細(xì)解釋。在清單2中,每行表達(dá)式都是等價(jià)的。x的函數(shù)被當(dāng)作參數(shù)傳遞給函數(shù)f,并且作用在3上。函數(shù)x作用在3上就得到了3+2。在函數(shù)式語(yǔ)言里,把一個(gè)函數(shù)作用在另一個(gè)函數(shù)上是非常常見(jiàn)的一種做法。下面我們考慮清單3:
清單3 :用函數(shù)賦值
在清單3中,有3個(gè)函數(shù)。函數(shù)scale_by_2是以scale函數(shù)為參數(shù)并且作用在2上。它的返回值就相當(dāng)于 λ n.x * 2 。這個(gè)表達(dá)式。函數(shù)式開(kāi)發(fā)通常就是一層一層地組建這種類型的函數(shù)。
#p#
閉包(Closure)
函數(shù)式語(yǔ)言的另一個(gè)重要術(shù)語(yǔ)和關(guān)注點(diǎn)就是閉包。閉包在現(xiàn)在的各種編程語(yǔ)言中都很常見(jiàn),這個(gè)術(shù)語(yǔ)常用來(lái)表示一個(gè)方法引用(method reference)或者一個(gè)匿名函數(shù)(anonymous function)。技術(shù)上看,閉包就是動(dòng)態(tài)分配的一個(gè)含有代碼指針(code pointer)的數(shù)據(jù)結(jié)構(gòu),這個(gè)代碼指針指向一個(gè)計(jì)算函數(shù)結(jié)果的代碼片段以及一個(gè)受限變量(found variable)環(huán)境。閉包用來(lái)把一個(gè)函數(shù)和“私有”變量聯(lián)系起來(lái)。許多語(yǔ)言里的匿名函數(shù)就是用來(lái)實(shí)現(xiàn)這一目的的,這也常常是讓初學(xué)者看不懂的地方。
- Function powerFunctionFactory(int power) {
- int powerFunction(int base) {
- return pow(base, power);
- }
- return powerFunction;
- }
- Function square = powerFunctionFactory (2);
- square(3); // returns 9
- Function cube = powerFunctionFactory (3);
- cube(3); // returns 27
在清單4里,factory這個(gè)函數(shù)返回的是一個(gè)求冪次的函數(shù)。當(dāng)我們調(diào)用square函數(shù)時(shí),它所需要的power這個(gè)變量根本就不在作用域內(nèi),為什么這樣也有意義呢?powerFunctionFactory這個(gè)函數(shù)返回后按理來(lái)說(shuō)它的堆棧應(yīng)該也就隨之釋放了。cube函數(shù)也有相同的問(wèn)題,只不過(guò)它求的冪次不一樣。要實(shí)現(xiàn)這樣的語(yǔ)法要求這種語(yǔ)言必須保存變量值,并且要為所創(chuàng)建的每個(gè)函數(shù)保存變量值。這就稱為閉包。
閉包允許把自定義的行為作為函數(shù)參數(shù)傳遞,這就引出了另一個(gè)重要的術(shù)語(yǔ),“柯里化”(currying)。
柯里化(Currying)
柯里化這個(gè)名字聽(tīng)起來(lái)很深?yuàn)W,實(shí)際上它指的就是把一個(gè)多參數(shù)函數(shù)轉(zhuǎn)換成只需要單個(gè)參數(shù)的函數(shù)鏈的這種變換。因此,考慮一個(gè)函數(shù) foo(x, y) 它的結(jié)果是 z 的值,或者我們把它寫成 foo(x, y) -> z 。現(xiàn)在,我們得把它分解成多個(gè)函數(shù),每個(gè)函數(shù)都需要一個(gè)函數(shù)作為傳入?yún)?shù)或者返回值??闯鰜?lái)這種技術(shù)與λ演算之間的關(guān)系了嗎?
如果有 bar(x)->baz, baz(y)->z。這表示bar函數(shù)將以x為參數(shù)并且返回函數(shù)baz。然后當(dāng)baz以y為參數(shù)時(shí),它的結(jié)果就是z。因此,foo(x, y) -> z可以用如下方式表示:
bar(x) -> baz
baz(y) -> z
還是讓我們結(jié)合一個(gè)C#實(shí)例看看吧。下面這段代碼在C# 3.5里是正確的:
- Func< int, Func< int,int>> scale =
- x => y => x * y;
- var scaleBy2 = scale(2);
- scaleBy2 (100);
按正常做法,我們應(yīng)該編寫一個(gè)方法,以數(shù)值100和倍數(shù)2為參數(shù)。但是,采用函數(shù)式編程的方法,函數(shù)scale(2)返回一個(gè)可以用來(lái)給變量賦值的函數(shù)。我們把那個(gè)返回的函數(shù)稱為scaleBy2,當(dāng)然這很容易“鏈?zhǔn)健钡剡M(jìn)行下去。通過(guò)對(duì)一個(gè)函數(shù)引用進(jìn)行命名,我們就有了一個(gè)可以被調(diào)整至整個(gè)程序里面使用的函數(shù)了。如果你沒(méi)有搞清楚這些,沒(méi)有關(guān)系,我們下面將繼續(xù)探討函數(shù)式編程的基礎(chǔ)。
數(shù)據(jù)結(jié)構(gòu)
函數(shù)式語(yǔ)言中的數(shù)據(jù)結(jié)構(gòu)包括元組(tuples)和單體(monad)。元組是不可改變的對(duì)象序列。序列,鏈表和樹也是函數(shù)式語(yǔ)言中非常常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)。大部分的語(yǔ)言都提供對(duì)這些數(shù)據(jù)結(jié)構(gòu)的運(yùn)算符和庫(kù),以簡(jiǎn)化對(duì)它們的運(yùn)算。
一個(gè)單體是一個(gè)用來(lái)反映控制流程或者運(yùn)算的抽象數(shù)據(jù)類型。引入它的目的是為了避免使用可能帶來(lái)副作用(side effect)的語(yǔ)法來(lái)表達(dá)輸入輸出操作和狀態(tài)變化。
模式匹配(Pattern Matching)
模式匹配并不是函數(shù)式編程的創(chuàng)新,也不是專用于函數(shù)式編程。但是它和函數(shù)式語(yǔ)言有廣泛聯(lián)系,因?yàn)槠渌髁骶幊陶Z(yǔ)言大都沒(méi)有這一特性。模式匹配說(shuō)白了就是一種對(duì)值或者類型進(jìn)行匹配的簡(jiǎn)潔方法。如果你曾經(jīng)寫過(guò)很復(fù)雜的if,if/else,或者switch語(yǔ)句,那么你應(yīng)該已經(jīng)能認(rèn)識(shí)到模式識(shí)別的價(jià)值了。清單6是一個(gè)用Mathematica編寫的匹配程序,用于求一個(gè)斐波納契序列(Fibonacci sequence)。
- fib[0|1]:=1
- fib[n_]:= fib[n-1] + fib[n-2]
清單6 :用Mathematica寫的模式匹配范例
對(duì)0或1進(jìn)行匹配的結(jié)果是1.對(duì)其它任何數(shù)的匹配都會(huì)進(jìn)入fid這個(gè)遞歸調(diào)用里。要想找一個(gè)比這還簡(jiǎn)潔的求斐波納契序列的方法可真是不容易了。這是一種非常強(qiáng)大的技術(shù)。
原文:An Introduction to Functional Languages
作者:Ken Sipe
【相關(guān)閱讀】