哪種語言將統治多核時代 再看函數式語言特性

最近這幾年,軟件開發語言可謂是層出不窮。在這些新的編程語言中,最多的就是函數式語言。本文將向你介紹函數式語言的概念、術語、方法以及幾種典型的函數式語言。本文面向的讀者是那些已經懂得其它編程語言、但卻對函數式語言沒有了解的開發人員。

什麼是函數式語言?

如果你已經用面向對象的語言(例如Java和C#)寫了很長時間的代碼,那麼可能很難想象出另一種新的編程思維方式,而函數式語言恰恰做到了這一點。它的核心就是通過對算法進行功能分解,從而解決軟件問題。在函數式語言裏,函數是首要的。如果你是Java陣營出身,那應該能理解到這之間的差異。在Java中,實現某種方法的唯一形式就是將其作爲某個類的成員。

雖然最近風頭正勁的某些特殊語言引人注目,但是函數式語言其實也可以代表一門技術,而不僅僅是一種語言。我們可以用函數式編程的方式,用面向對象的編程語言實現一般的功能(後面的章節裏我們就會看到一個用函數式方法編寫的C#程序)。雖然這可以實現,但是在稍大一點的程序中,我們很快就會感覺到缺乏表現力,反模式(anti-pattern)也隨之出現。試想一下,不用extend和implement關鍵字寫個上規模的Java程序有多痛苦。由於這些困難的存在,人們需要一種新的語言:一種函數式語言

爲何需要函數式語言?

很明顯,現代計算平臺上發生了一個重大變化,那就是多核技術的引入。除了上網本和PDA,我們甚至都找不出還採用單核處理器的臺式機和筆記本電腦了。我們正在向多核心,多處理器發展,並且所有跡象都表明這一趨勢將繼續下去。除了採用多核心之外,高運算量和高複雜度的算法應用都傾向於優化使用圖形處理單元(GPU),從而提高並行性。歸納起來,從開發者的角度來說,這些都屬於併發問題的範疇。

我們大多數的編程語言都不容易實現併發。想想幾十年前,C語言程序裏的錯誤處理直接就被代碼基底(code base)丟棄了。它與業務邏輯混在了一起。C函數成功後就將返回0 ,失敗則返回錯誤代碼。很明顯這不是很理想的辦法,但是C語言本身的表達能力限制了開發者們用其它方法來進行出錯處理。其他語言對此進行了改進,在C++或者Java中,出錯時會拋出異常,異常處理程序把出錯處理和正常事務處理分離開來。有些人可能會說這也算不上什麼太好的辦法,但是這至少是個不小的進步了。在解決併發這個問題上,我們所掌握的技術的成熟度也和這差不多。如果想要在一個用面嚮對象語言編程的程序裏實現並行,那編寫起來真得費一番腦筋。像生成一個打印任務線程並不需要處理多少併發控制,但是往往它還會牽扯到進程間的狀態共享顯示器的阻塞。隨着內核數的增加,同時運行的線程數可能進一步增大,系統的效率也將隨之降低。這時,我們就需要一種新的語言,讓我們能從這些細節工作中抽身出來,以更好地利用併發。

函數式語言已經在簡化並行開發中證明了它的作用, 這得益於它既不用共享內存,也不會產生副作用(side effect)的函數。進一步深入函數式語言,你就會發現它讓開發者從併發這個概念中抽身出來了,讓開發者不用老是想着現在CPU是在併發作業。許多語言實現了一種併發開發模式,通常稱之爲Actor模型。在這種模型下,進程間傳遞消息而不是共享狀態從而消除線程阻塞

函數式語言的另一大寶貴優點就是簡潔。在Stuart Halloway的《Programming Clojure》一書第一章中,Stuart展示了3行clojure代碼,這比用Jakarta Commons框架開發減小了三分之一的代碼量,同時還體現出更清晰明瞭的邏輯思路。

有一個很重要的觀點就是,函數式語言不是用來取代面向過程或者面向對象的編程語言的。看看我們在上一節中所列舉出來的幾種函數式語言,就會發現許多新的函數式語言都是多範型(multi-paradigm),很多時候這些語言都是運行在虛擬機上,並且作爲其它面嚮對象語言和命令式語言的橋樑出現的。選擇適合手頭工作的語言纔是最重要的。我希望從業者們在開發主流應用時繼續使用Java,Groovy或者C#這些通用語言,但當面臨着一個極爲複雜的算法或需要實現高併發時,最好還是轉而用函數式語言來集成這些方案。這也正是Neal Ford說了多年的 “多語言程序員”(polyglot programmer)。對於此類程序員的形成,我們可以參考一下一位Java兼Scala開發者的學習歷程

幾種典型的函數式語言

當我們回顧歷史上的編程語言時,就會發現其實函數式語言並不是一個新生事物,它早就出現過。其中最廣爲人知的幾種“祖父”級語言包括:LISP和FORTRAN 。自1980年代中期以來,這些語言在企業和商業開發領域逐漸讓位於面向對象的開發語言,流行領域也逐漸縮小到只剩下學術界。不過下面列出的這幾種函數式語言最近正在向商業領域發起反攻

◆Erlang:這是一種以A.K Erlang的名字命名的通用並行編程語言。它有函數式語言的元素,以及一個Actor 併發模型,從而簡化並行開發工作。編輯推薦對Erlang感興趣的讀者閱讀一下51CTO以前的一次訪談:因併發而生 因雲計算而熱:Erlang專家訪談實錄

◆Haskell:這是一門已經有超過20年曆史的開源編程語言,它的設計宗旨就是成爲一門純粹的函數式語言。

◆OCaml:面向對象的Caml(Objective Caml)是Caml語言的一個開源版本,Caml語言可以算是ML語言的一個方言版了,ML語言1970年就已經開發出來了,也是作爲一種通用函數式語言存在的。它被認爲是後來出現的F#等多種函數式語言的基礎。

◆Lisp:表處理語言(List Processing Language)是一種函數式語言,最初是於1958年擬定的。由它派生出了許多分支。

◆Scala:Scala 語言的設計目標是在Java虛擬機上實現函數式和麪向對象這兩類編程語言的集成。它是一種強類型的編程語言。Scala編程語言近年來的流行度在不斷提升,編輯推薦讀者參閱51CTO的Scala編程語言專題

◆Clojure:Clojure是Lisp語言的一個現代分支,它運行在Java虛擬機上,是爲併發程序開發設計的。它是一種動態類型編程語言。

◆F#:這是一種運行在.Net CLR平臺上的新語言。它是OCaml的一個分支,它兼具了函數式和命令式面嚮對象語言的特點。同時它也是一種強類型的編程語言。F#在未來的.NET平臺上有重要的作用,將在Visual Studio 2010中被正式包含

值得注意的是函數式語言並不一定要是動態語言(dynamic language)。函數式語言允許動態或靜態類型。這裏所列出的語言只是各種各樣函數式語言中的一個子集,每一種實現了某種特定的需求。本文將介紹好幾種典型函數式語言,而不是專門講解某一種語言。另外我們還有一個沒有回答的問題就是:爲什麼現在對函數式語言的需求越來越強烈?

 

 

函數式語言的特色

函數式語言的根本宗旨之一就是它不是一種命令式(imperative)語言。在命令式語言中,函數中定義的變量通常都代表內存中的一塊特定大小的區域,而且賦給它的值也通常允許在整個方法中改變。而在函數式語言中,對變量的賦值是綁定性的,就像在數學函數式中那樣。比如說,有這樣的數學式:let x=2。這就是說對於這個問題,x的值爲2。x的值不能改變,總是2。按照這樣的模式,我們平常編程過程中的一些寫法就沒有意義了,例如這個賦值語句:let x=x+1。在命令式語言中這是有意義的,但是在數學上,這是沒有任何意義的,因爲x=x+1是無解的。理解這個概念後,函數式開發(functional development)就算是上路了。

和在數學中一樣,函數式語言中的賦值並不僅限於數值型。方法在函數式語言中是最爲重要的。因此,一個方法閉包(method closure)可以用來給變量賦值,並且被傳遞或調整到其它的函數表達式裏。用數學表達式來說就相當於:let x=f(y)。數學上稱之爲:x是以y爲自變量的函數f 的函數值。任給一個y值,都有一個對應的x值。這就是函數式編程的另一個核心思想。只要y不變,那麼x也總是一個特定的對應值,不會發生改變。

雖然不同的函數式語言之間有一些不同之處,但是他們都有以下一些共同點:

◆函數閉包支持

◆高階函數

◆用for流程來實現遞歸

◆沒有副作用(side-effects)

◆把重點放在“要計算什麼”,而不是“如何去計算”上。

◆引用透明性(Referential transparency)

函數式語言的功能和術語

伴隨着函數式語言的發展,涌現了許多新的術語,但是沒有哪種能比Lambda產生得更快。就像我們前面提到的一樣,函數式編程和數學界有很大的聯繫。Lambda指的是λ演算(lambda calculus 或 l-calculus)λ演算是一套用於研究函數定義、函數應用和函數遞歸的系統。

一個簡單的Lambda表達式 

清單1 :一個簡單的Lambda表達式

還有許多的λ表達式我們這裏都不再深入討論了。如清單1所示的幾個簡單表達式,λ提供了一種全新的語法。上面所舉的例子表示的是一個一元函數,這意味着函數只需要一個參數,或者說元數是1.在清單2中,我們可以看到把一個函數作爲另一個函數的參數。

一個簡單的λ函數作參數傳遞 

清單2 :一個簡單的λ函數作參數傳遞

λ表達式在線指引上對這些有詳細解釋。在清單2中,每行表達式都是等價的。x的函數被當作參數傳遞給函數f,並且作用在3上。函數x作用在3上就得到了3+2。在函數式語言裏,把一個函數作用在另一個函數上是非常常見的一種做法。下面我們考慮清單3:

用函數賦值 

清單3 :用函數賦值

在清單3中,有3個函數。函數scale_by_2是以scale函數爲參數並且作用在2上。它的返回值就相當於 λ n.x * 2 。這個表達式。函數式開發通常就是一層一層地組建這種類型的函數。

 

 

 

閉包(Closure)

函數式語言的另一個重要術語和關注點就是閉包。閉包在現在的各種編程語言中都很常見,這個術語常用來表示一個方法引用(method reference)或者一個匿名函數(anonymous function)。技術上看,閉包就是動態分配的一個含有代碼指針(code pointer)的數據結構,這個代碼指針指向一個計算函數結果的代碼片段以及一個受限變量(found variable)環境。閉包用來把一個函數和“私有”變量聯繫起來。許多語言裏的匿名函數就是用來實現這一目的的,這也常常是讓初學者看不懂的地方。

  1. Function powerFunctionFactory(int power) {  
  2.    int powerFunction(int base) {  
  3.        return pow(base, power);  
  4.    }  
  5.    return powerFunction;  
  6. }  
  7. Function square = powerFunctionFactory (2);  
  8. square(3); // returns 9  
  9. Function cube = powerFunctionFactory (3);  
  10. cube(3); // returns 27 

在清單4裏,factory這個函數返回的是一個求冪次的函數。當我們調用square函數時,它所需要的power這個變量根本就不在作用域內,爲什麼這樣也有意義呢?powerFunctionFactory這個函數返回後按理來說它的堆棧應該也就隨之釋放了。cube函數也有相同的問題,只不過它求的冪次不一樣。要實現這樣的語法要求這種語言必須保存變量值,並且要爲所創建的每個函數保存變量值。這就稱爲閉包。

閉包允許把自定義的行爲作爲函數參數傳遞,這就引出了另一個重要的術語,“柯里化”(currying)。

柯里化(Currying)

柯里化這個名字聽起來很深奧,實際上它指的就是把一個多參數函數轉換成只需要單個參數的函數鏈的這種變換。因此,考慮一個函數 foo(x, y) 它的結果是 z 的值,或者我們把它寫成 foo(x, y) -> z 。現在,我們得把它分解成多個函數,每個函數都需要一個函數作爲傳入參數或者返回值。看出來這種技術與λ演算之間的關係了嗎?

如果有 bar(x)->baz, baz(y)->z。這表示bar函數將以x爲參數並且返回函數baz。然後當baz以y爲參數時,它的結果就是z。因此,foo(x, y) -> z可以用如下方式表示:

bar(x) -> baz

baz(y) -> z

還是讓我們結合一個C#實例看看吧。下面這段代碼在C# 3.5裏是正確的:

  1. Func< int, Func< int,int>> scale =  
  2.    x => y => x * y;  
  3.  
  4. var scaleBy2 = scale(2);  
  5. scaleBy2 (100); 

按正常做法,我們應該編寫一個方法,以數值100和倍數2爲參數。但是,採用函數式編程的方法,函數scale(2)返回一個可以用來給變量賦值的函數。我們把那個返回的函數稱爲scaleBy2,當然這很容易“鏈式”地進行下去。通過對一個函數引用進行命名,我們就有了一個可以被調整至整個程序裏面使用的函數了。如果你沒有搞清楚這些,沒有關係,我們下面將繼續探討函數式編程的基礎。

數據結構

函數式語言中的數據結構包括元組(tuples)和單體(monad)。元組是不可改變的對象序列。序列,鏈表和樹也是函數式語言中非常常見的數據結構。大部分的語言都提供對這些數據結構的運算符和庫,以簡化對它們的運算。

一個單體是一個用來反映控制流程或者運算的抽象數據類型。引入它的目的是爲了避免使用可能帶來副作用(side effect)的語法來表達輸入輸出操作和狀態變化。

模式匹配(Pattern Matching)

模式匹配並不是函數式編程的創新,也不是專用於函數式編程。但是它和函數式語言有廣泛聯繫,因爲其它主流編程語言大都沒有這一特性。模式匹配說白了就是一種對值或者類型進行匹配的簡潔方法。如果你曾經寫過很複雜的if,if/else,或者switch語句,那麼你應該已經能認識到模式識別的價值了。清單6是一個用Mathematica編寫的匹配程序,用於求一個斐波納契序列(Fibonacci sequence)。

  1. fib[0|1]:=1   
  2. fib[n_]:fib[n-1] + fib[n-2] 

清單6 :用Mathematica寫的模式匹配範例

對0或1進行匹配的結果是1.對其它任何數的匹配都會進入fid這個遞歸調用裏。要想找一個比這還簡潔的求斐波納契序列的方法可真是不容易了。這是一種非常強大的技術。

原文:An Introduction to Functional Languages

作者:Ken Sipe

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