scala函數式編程

本篇博客內容徹底是摘自 IBM developerWorks,非原創,感受文章不錯,遂轉至本博客。原文 http://www.ibm.com/developerworks/cn/java/j-lo-funinscala3/java

Scala 是一種有趣的語言。它一方面吸取繼承了多種語言中的優秀特性,一方面又沒有拋棄 Java 這個強大的平臺,它運行在 JVM 之上,輕鬆實現和豐富的 Java 類庫互聯互通。它既支持面向對象的編程方式,又支持函數式編程。它寫出的程序像動態語言同樣簡潔,但事實上它倒是嚴格意義上的靜態語言。Scala 就像一位武林中的集大成者,將過去幾十年計算機語言發展歷史中的精萃集於一身,化繁爲簡,爲程序員們提供了一種新的選擇。做者但願經過這個系列,能夠爲你們介紹 Scala 語言的特性,和 Scala 語言給咱們帶來的關於編程思想的新的思考。本文將帶領你們一塊兒回顧函數式編程的歷史,清楚函數式編程的定義,並以一個例子,由易到難爲你們展現函數式編程的優勢,最後介紹了柯里化的概念。程序員

函數式編程是這幾年很受歡迎的一個話題,即便你是一個剛剛踏入職場的新人,若是在面試時能有意無心地透露出你懂那麼一點點函數式編程,也會讓你的面試官眼前一亮。然而函數式編程並非一個新的概念,它的源頭能夠追溯到計算機還沒有發明以前。本文將帶領你們回顧一下函數式編程的歷史,並使用 Scala 語言爲你們講解函數式編程的基本概念。面試

函數式編程的歷史

有機會看到這篇文章的讀者,大概都會知道阿蘭·圖靈(Alan Turing)和約翰·馮·諾伊曼(John von Neumann)。阿蘭·圖靈提出了圖靈機的概念,約翰·馮·諾伊曼基於這一理論,設計出了第一臺現代計算機。因爲圖靈以及馮·諾伊曼式計算機的大獲成功,歷史差點淹沒了另一位一樣傑出的科學家和他的理論,那就是阿隆佐·邱奇(Alonzo Church)和他的λ演算。阿隆佐·邱奇是阿蘭·圖靈的老師,上世紀三十年代,他們一塊兒在普林斯頓研究可計算性問題,爲了回答這一問題,阿隆佐·邱奇提出了λ演算,其後不久,阿蘭·圖靈提出了圖靈機的概念,儘管形式不一樣,但後來證實,兩個理論在功能上是等價的,條條大路通羅馬。若是不是約翰·麥卡錫(John McCarthy),阿隆佐·邱奇的λ演算恐怕還要在歷史的故紙堆中再多躺幾十年,約翰·麥卡錫是人工智能科學的奠定人之一,他發現了λ演算的珍貴价值,發明了基於λ演算的函數式編程語言:Lisp,因爲其強大的表達能力,一推出就受到學術界的熱烈歡迎,以致於一段時間內,Lisp 成了人工智能領域的標準編程語言。很快,λ演算在學術界流行開來,出現了不少函數式編程語言:Scheme 、SML、Ocaml 等,可是在工業界,仍是命令式編程語言的天下,Fortran、C、C++、Java 等。隨着時間的流逝,愈來愈多的計算機從業人員認識到函數式編程的意義,愛立信公司於上世紀八十年代開發出了 Erlang 語言來解決併發編程的問題;在互聯網的發展浪潮中,愈來愈多的語言也開始支持函數式編程:JavaScript、Python、Ruby、Haskell、Scala 等。能夠預見,若是過去找一個懂什麼是函數式編程的程序員很困難,那麼在不久的未來,找一個一點也沒聽過函數式編程的程序員將更加困難。算法

什麼是函數式編程

狹義地說,函數式編程沒有可變的變量、循環等這些命令式編程方式中的元素,像數學裏的函數同樣,對於給定的輸入,無論你調用該函數多少次,永遠返回一樣的結果。而在咱們經常使用的命令式編程方式中,變量用來描述事物的狀態,整個程序,不過是根據不斷變化的條件來維護這些變量。編程

廣義地說,函數式編程重點在函數,函數是這個世界裏的一等公民,函數和其餘值同樣,能夠處處被定義,能夠做爲參數傳入另外一個函數,也能夠做爲函數的返回值,返回給調用者。利用這些特性,能夠靈活組合已有函數造成新的函數,能夠在更高層次上對問題進行抽象。本文的重點將放在這一部分。併發

函數式編程有什麼優勢

約翰·巴克斯(John Backus)爲人熟知的兩項成就是 FORTRAN 語言和用於描述形式系統的巴克斯範式,由於這兩項成就,他得到了 1977 年的圖靈獎。有趣的是他在獲獎後,作了一個關於函數式編程的講演:Can Programming Be Liberated From the von Neumann Style? 1977 Turing Award Lecture。他認爲像 FORTRAN 這樣的命令式語言不夠高級,應該有新的,更高級的語言能夠擺脫馮諾依曼模型的限制,因而他又發明了 FP 語言,雖然這個語言未獲成功,可是約翰·巴克斯關於函數式編程的論述卻獲得了愈來愈多的承認。下面,咱們就羅列一些函數式編程的優勢。編程語言

首先,函數式編程自然有併發的優點。因爲工藝限制,摩爾定律已經失效,芯片廠商只能採起多核策略。程序要利用多核運算,必須採起併發,而併發最頭疼的問題就是共享數據,狹義的函數式編程沒有可變的變量,數據只讀不寫,併發的問題迎刃而解。這也是前面兩篇文章中,一直建議你們在定義變量時,使用 val 而不是 var 的緣由。愛立信公司發明的 Erlang 語言就是爲解決併發的問題而生,在電信行業取得了不俗的成績。函數式編程

其次,函數式編程有跡可尋。因爲不依賴外部變量,給定輸入函數的返回結果永遠不變,對於複雜的程序,咱們能夠用值替換的方式(substitution model)化繁爲簡,輕鬆得出一段程序的計算結果。爲這樣的程序寫單元測試也很方便,由於不用擔憂環境的影響。函數

最後,函數式編程高屋建瓴。寫程序最重要的就是抽象,不一樣風格的編程語言爲咱們提供了不一樣的抽象層次,抽象層次越高,表達問題越簡潔,越優雅。讀者從下面的例子中能夠看到,使用函數式編程,有一種高屋建瓴的感受。單元測試

抽象,抽象,再抽象!

說了這麼多,相信不少性急的讀者都等不及想看看怎麼使用 Scala 進行函數式編程了吧。那麼,先請你們暫時忘掉之前命令式編程的經驗,用一個全新的大腦來開始這段函數式編程之旅。

故事從我上初中的外甥小龍身上開始,像全部聰明的孩子同樣,小龍身上具有了懶,不耐煩以及妄自尊大這些優秀特質。他厭倦了數學做業上那些大量沒有意義的,重複的練習題。還好他有個當程序員的姨夫:在電腦上裝個 Scala,寫程序作吧。因而小龍把 Scala 看成一個計算器,寫出了他有生以來第一段程序:

清單 1. 求立方
   35 * 35 * 35 
   68 * 68 * 68 
   // 如下省去大量重複的,沒有意義的練習題

做業作完了,雖然大腦獲得了休息,可是小龍的手累壞了!做爲一個懶人,小龍是不會知足於不動腦,但要動手這種情況的。因而,我教給了他最基本的抽象方式:將算法抽象爲一個函數。小龍很快作完做業,高高興興跟小夥伴們打籃球去了。

清單 2. 求立方函數
   def cube(n: Int) = n * n * n 
   // 有了這個函數,小龍作起做業輕鬆多了
   cube(35) 
   cube(68) 
   // 如下省去大量重複的,沒有意義的練習題

隨着教學進度的加快,小龍的做業也愈來愈難了,很快,小龍遇到了這樣的題目:求出 1 到 10 的立方和。聰明如小龍,或者說懶惰如小龍,在前一個函數基礎之上,很快又定義了個新函數,仍是個遞歸函數!沒錯,在小龍還沒看見過循環以前,我先教會了他遞歸,他理解起來絕不費力:對 a 到 b 之間的數求立方和,等於 a 的立方和,加上 (a + 1) 到 b 之間的數的立方和。若是讀者對於遞歸還有疑惑,請參考做者的前一篇文章《使用遞歸的方式去思考》。小龍又很快作完做業,高高興興跟着小夥伴們打球去了。

清單 3. 求立方和
   def cube(n: Int) = n * n * n 
   def sumCube(a: Int, b: Int): Int = 
 if (a > b) 0 else cube(a) + sumCube(a + 1, b) 

   // 有了這個函數,小龍作起做業輕鬆多了
   sumCube(1, 10)

因禍得福,焉知非福,好事很快變壞事,因爲小龍數學做業作得又快又好,被老師選拔爲奧數培養對象,除過做業,小龍天天還要作大量的額外練習:求 1 到 10 的和,求 1 到 10 的平方和,求 1 到 10 的階乘和等等。這時,小龍已經對定義函數很熟練了,三下五除二,小龍又定義出一堆函數出來。

清單 4. 各類求和函數
   def cube(n: Int) = n * n * n 
   def id(n: Int) = n 
   def square(n : Int) = n * n 
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   def sumCube(a: Int, b: Int): Int = 
    if (a > b) 0 else cube(a) + sumCube(a + 1, b) 

   def sumSquare(a: Int, b: Int): Int = 
    if(a > b) 0 else square(a) + sumSquare(a + 1, b) 
    
   def sumFact(a: Int, b: Int): Int = 
    if (a > b) 0 else fact(a) + sumFact(a + 1, b) 

   def sumInt(a: Int, b: Int): Int = 
 if(a > b) 0 else id(a) + sumInt(a + 1, b)   

   // 有了這些函數,小龍作起做業輕鬆多了
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

問題解決了,但小龍總以爲哪裏不對勁,(這時,一個畫外音高喊:Don ’ t Repeat Yourself!),是的,仔細觀察小龍定義的這四個求和函數,幾乎是如出一轍的。能不能也將這些如出一轍的東西抽象出來?我以爲是時候教給他第二項本領了:高階函數(Higher-Order Function),所謂高階函數,就是操做其餘函數的函數。以求和爲例,咱們能夠定義一個新的求和函數,該函數接受另一個函數做爲參數,這個做爲參數的函數表明了某種對數據的操做。使用高階函數後,抽象層次提升,代碼變得更簡單了。

清單 5. 使用高階函數定義求和函數
   def cube(n: Int) = n * n * n 
   def id(n: Int) = n 
   def square(n : Int) = n * n 
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高階函數
   def sum(f: Int => Int, a: Int, b: Int): Int = 
    if (a > b) 0 else f(a) + sum(f, a + 1, b) 

   // 使用高階函數從新定義求和函數
   def sumCube(a: Int, b: Int): Int = sum(cube, a, b) 
   def sumSquare(a: Int, b: Int): Int = sum(square, a, b) 
   def sumFact(a: Int, b: Int): Int = sum(fact, a, b) 
   def sumInt(a: Int, b: Int): Int = sum(id, a, b) 

   // 有了這些函數,小龍作起做業輕鬆多了
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

對於簡單的函數,咱們還能夠將其轉化爲匿名函數,讓程序變得更簡潔一些。在高階函數中使用匿名函數,這是函數式編程中常常用到的一個技巧,多數狀況下,咱們關心的是高階函數,而不是做爲參數傳入的函數,因此爲其單獨定義一個函數是沒有必要的。值得稱讚的是 Scala 中定義匿名函數的語法很簡單,箭頭左邊是參數列表,右邊是函數體,參數的類型是可省略的,Scala 的類型推測系統會推測出參數的類型。使用匿名函數後,咱們的代碼變得更簡潔了:

清單 6. 在高階函數中使用匿名函數
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高階函數
   def sum(f: Int => Int, a: Int, b: Int): Int = 
    if (a > b) 0 else f(a) + sum(f, a + 1, b) 

   // 使用高階函數從新定義求和函數
   def sumCube(a: Int, b: Int): Int = sum(x => x * x * x, a, b) 
   def sumSquare(a: Int, b: Int): Int = sum(x => x * x, a, b) 
   def sumFact(a: Int, b: Int): Int = sum(fact, a, b) 
   def sumInt(a: Int, b: Int): Int = sum(x => x, a, b) 

   // 有了這些函數,小龍作起做業輕鬆多了
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

小龍的故事到此就結束了,但願讀者能從這一例子中,體會出函數式編程的一些精妙之處。下面咱們將進入函數式編程的另外一個概念:柯里化(Currying)

回頁首

柯里化

做爲一個程序員,應該永遠有一顆追求完美的心,上面使用匿名函數後的高階函數還有什麼地方值得改進呢?但願你們還會想起那句話:Don ’ t Repeat Yourself !求和函數的兩個上下限參數 a,b被重複得傳來傳去。咱們試着從新定義 sum函數,讓它接受一個函數做爲參數,同時返回另一個函數。看到沒?使用新的 sum函數,咱們再定義各類求和函數時,徹底不須要這兩個上下限參數了,咱們的程序又一次獲得了簡化。

清單 7. 返回函數的高階函數
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高階函數
   def sum(f: Int => Int): (Int, Int) => Int = { 
    def sumF(a: Int, b: Int): Int = 
      if (a > b) 0 else f(a) + sumF(a + 1, b) 

    sumF 
  } 
   // 使用高階函數從新定義求和函數
   def sumCube: Int = sum(x => x * x * x) 
   def sumSquare: Int = sum(x => x * x) 
   def sumFact: Int = sum(fact) 
   def sumInt: Int = sum(x => x) 

   // 這些函數使用起來還和原來同樣 ! 
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

能不能再簡單一點呢?既然 sum返回的是一個函數,咱們應該能夠直接使用這個函數,彷佛沒有必要再定義各類求和函數了。

清單 8. 直接調用高階函數
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高階函數
   def sum(f: Int => Int): (Int, Int) => Int = { 
    def sumF(a: Int, b: Int): Int = 
      if (a > b) 0 else f(a) + sumF(a + 1, b) 

    sumF 
  } 
   // 這些函數沒有必要了
   //def sumCube: Int = sum(x => x * x * x) 
   //def sumSquare: Int = sum(x => x * x) 
   //def sumFact: Int = sum(fact) 
   //def sumInt: Int = sum(x => x) 

   // 直接調用高階函數 ! 
   sum(x => x * x * x) (1, 10) //=> sumCube(1, 10) 
   sum(x => x) (1, 10)           //=> sumInt(1, 10) 
   sum(x => x * x) (1, 10)      //=> sumSquare(1, 10) 
   sum(fact) (1, 10)             //=>  sumFact(1, 10)

這種返回函數的高階函數極爲有用,所以 Scala 爲其提供了語法糖,上面的 sum函數能夠簡寫爲:

清單 9. 高階函數的語法糖
   // 沒使用語法糖的 sum 函數
   def sum(f: Int => Int): (Int, Int): Int = { 
    def sumF(a: Int, b: Int): Int = 
      if (a > b) 0 else f(a) + sumF(a + 1, b) 

    sumF 
  } 
  // 使用語法糖後的 sum 函數
   def sum(f: Int => Int)(a: Int, b: Int): Int = 
    if (a > b) 0 else f(a) + sum(f)(a + 1, b)

讀者可能會問:咱們把原來的 sum函數轉化成這樣的形式,好處在哪裏?答案是咱們得到了更多的可能性,好比剛開始求和的上下限還沒肯定,咱們能夠在程序中把一個函數傳給 sumsum(fact)徹底是一個合法的表達式,待後續上下限肯定下來時,再把另外兩個參數傳進來。對於 sum 函數,咱們還能夠更進一步,把 a,b 參數再轉化一下,這樣 sum 函數就變成了這樣一個函數:它每次只能接收一個參數,而後返回另外一個接收一個參數的函數,調用後,又返回一個只接收一個參數的函數。這就是傳說中的柯里化,多麼完美的形式!在現實世界中,的確有這樣一門函數式編程語言,那就是 Haskell,在 Haskell 中,全部的函數都是柯里化的,即全部的函數只接收一個參數!

清單 10. 柯里化
  // 柯里化後的 sum 函數
   def sum(f: Int => Int)(a: Int) (b: Int): Int = 
 if (a > b) 0 else f(a) + sum(f)(a + 1)(b) 

 // 使用柯里化後的高階函數 ! 
   sum(x => x * x * x)(1)(10) //=> sumCube(1, 10) 
   sum(x => x)(1)(10)           //=> sumInt(1, 10)

結束語

本文和你們一塊兒回顧了函數式編程的歷史,並使用了大量示例代碼幫助你們理解函數式編程中的基本概念。在 Scala 類庫中,使用函數式編程的例子比比皆是,特別是對於列表的操做,將高階函數的優點展現得淋漓盡致,限於篇幅,不能在本文中爲你們做以介紹,做者將在後面的系列文章中,以 Scala 中的列表爲例,詳細介紹高階函數在實戰中的應用。

相關文章
相關標籤/搜索