首頁»編程語言»函數式編程初探

函數式編程初探

來源:ruanyifeng 發布時間:2012-08-20 閱讀次數:

  誕生50多年之后,函數式編程(functional programming)開始獲得越來越多的關注。

  不僅最古老的函數式語言Lisp重獲青春,而且新的函數式語言層出不窮,比如Erlang、clojure、Scala、F#等等。目前最當紅的Python、Ruby、Javascript,對函數式編程的支持都很強,就連老牌的面向對象的Java、面向過程的PHP,都忙不迭地加入對匿名函數的支持。越來越多的跡象表明,函數式編程已經不再是學術界的最愛,開始大踏步地在業界投入實用。

  也許繼"面向對象編程"之后,"函數式編程"會成為下一個編程的主流范式(paradigm)。未來的程序員恐怕或多或少都必須懂一點。

  但是,“函數式編程”看上去比較難,缺乏通俗的入門教程,各種介紹文章都充斥著數學符號和專用術語,讓人讀了如墜云霧。就連最基本的問題“什么是函數式編程”,網上都搜不到易懂的回答。

  下面是我的“函數式編程”學習筆記,分享出來,與大家一起探討。內容不涉及數學(我也不懂Lambda Calculus),也不涉及高級特性(比如lazy evaluationcurrying),只求盡量簡單通俗地整理和表達,我現在所理解的"函數式編程"以及它的意義。

  我主要參考了Slava Akhmechet的"Functional Programming For The Rest of Us"

 一、定義

  簡單說,"函數式編程"是一種"編程范式"(programming paradigm),也就是如何編寫程序的方法論。

  它屬于"結構化編程"的一種,主要思想是把運算過程盡量寫成一系列嵌套的函數調用。舉例來說,現在有這樣一個數學表達式:

  (1 + 2) * 3 - 4

  傳統的過程式編程,可能這樣寫:

  var a = 1 + 2;

  var b = a * 3;

  var c = b - 4;

  函數式編程要求使用函數,我們可以把運算過程定義為不同的函數,然后寫成下面這樣:

  var result = subtract(multiply(add(1,2), 3), 4);

  這就是函數式編程。

 二、特點

  函數式編程具有五個鮮明的特點。

  1. 函數是"第一等公民"

  所謂"第一等公民"(first class),指的是函數與其他數據類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。

舉例來說,下面代碼中的print變量就是一個函數,可以作為另一個函數的參數。

  var print = function(i){ console.log(i);};

  [1,2,3].forEach(print);

  2. 只用"表達式",不用"語句"

  "表達式"(expression)是一個單純的運算過程,總是有返回值;"語句"(statement)是執行某種操作,沒有返回值。函數式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,而且都有返回值。

  原因是函數式編程的開發動機,一開始就是為了處理運算(computation),不考慮系統的讀寫(I/O)。"語句"屬于對系統的讀寫操作,所以就被排斥在外。

  當然,實際應用中,不做I/O是不可能的。因此,編程過程中,函數式編程只要求把I/O限制到最小,不要有不必要的讀寫行為,保持計算過程的單純性。

  3. 沒有"副作用"

  所謂"副作用"(side effect),指的是函數內部與外部互動(最典型的情況,就是修改全局變量的值),產生運算以外的其他結果。

  函數式編程強調沒有"副作用",意味著函數要保持獨立,所有功能就是返回一個新的值,沒有其他行為,尤其是不得修改外部變量的值。

  4. 不修改狀態

  上一點已經提到,函數式編程只是返回新的值,不修改系統變量。因此,不修改變量,也是它的一個重要特點。

  在其他類型的語言中,變量往往用來保存"狀態"(state)。不修改變量,意味著狀態不能保存在變量中。函數式編程使用參數保存狀態,最好的例子就是遞歸。下面的代碼是一個將字符串逆序排列的函數,它演示了不同的參數如何決定了運算所處的"狀態"。

  function reverse(string) {

    if(string.length == 0) {

      return string;

    } else {

      return reverse(string.substring(1, string.length)) + string.substring(0, 1);

    }

  }

  由于使用了遞歸,函數式語言的運行速度比較慢,這是它長期不能在業界推廣的主要原因。

  5. 引用透明

  引用透明(Referential transparency),指的是函數的運行不依賴于外部變量或"狀態",只依賴于輸入的參數,任何時候只要參數相同,引用函數所得到的返回值總是相同的。

  有了前面的第三點和第四點,這點是很顯然的。其他類型的語言,函數的返回值往往與系統狀態有關,不同的狀態之下,返回值是不一樣的。這就叫"引用不透明",很不利于觀察和理解程序的行為。

 三、意義

  函數式編程到底有什么好處,為什么會變得越來越流行?

  1. 代碼簡潔,開發快速

  函數式編程大量使用函數,減少了代碼的重復,因此程序比較短,開發速度較快。

  Paul Graham在《黑客與畫家》一書中寫道:同樣功能的程序,極端情況下,Lisp代碼的長度可能是C代碼的二十分之一。

  如果程序員每天所寫的代碼行數基本相同,這就意味著,"C語言需要一年時間完成開發某個功能,Lisp語言只需要不到三星期。反過來說,如果某個新功能,Lisp語言完成開發需要三個月,C語言需要寫五年。"當然,這樣的對比故意夸大了差異,但是"在一個高度競爭的市場中,即使開發速度只相差兩三倍,也足以使得你永遠處在落后的位置。"

  2. 接近自然語言,易于理解

  函數式編程的自由度很高,可以寫出很接近自然語言的代碼。

  前文曾經將表達式(1 + 2) * 3 - 4,寫成函數式語言:

  subtract(multiply(add(1,2), 3), 4)

  對它進行變形,不難得到另一種寫法:

  add(1,2).multiply(3).subtract(4)

  這基本就是自然語言的表達了。再看下面的代碼,大家應該一眼就能明白它的意思吧:

  merge([1,2],[3,4]).sort().search("2")

  因此,函數式編程的代碼更容易理解。

  3. 更方便的代碼管理

  函數式編程不依賴、也不會改變外界的狀態,只要給定輸入參數,返回的結果必定相同。因此,每一個函數都可以被看做獨立單元,很有利于進行單元測試(unit testing)和除錯(debugging),以及模塊化組合。

  4. 易于"并發編程"

  函數式編程不需要考慮"死鎖"(deadlock),因為它不修改變量,所以根本不存在"鎖"線程的問題。不必擔心一個線程的數據,被另一個線程修改,所以可以很放心地把工作分攤到多個線程,部署"并發編程"(concurrency)。

  請看下面的代碼:

  var s1 = Op1();

  var s2 = Op2();

  var s3 = concat(s1, s2);

  由于s1和s2互不干擾,不會修改變量,誰先執行是無所謂的,所以可以放心地增加線程,把它們分配在兩個線程上完成。其他類型的語言就做不到這一點,因為s1可能會修改系統狀態,而s2可能會用到這些狀態,所以必須保證s2在s1之后運行,自然也就不能部署到其他線程上了。

  多核CPU是將來的潮流,所以函數式編程的這個特性非常重要。

  5. 代碼的熱升級

  函數式編程沒有副作用,只要保證接口不變,內部實現是外部無關的。所以,可以在運行狀態下直接升級代碼,不需要重啟,也不需要停機。Erlang語言早就證明了這一點,它是瑞典愛立信公司為了管理電話系統而開發的,電話系統的升級當然是不能停機的。

相關文檔:為什么Lisp語言如此先進?

QQ群:WEB開發者官方群(515171538),驗證消息:10000
微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
網友評論(共0條評論) 正在載入評論......
理智評論文明上網,拒絕惡意謾罵 發表評論 / 共0條評論
登錄會員中心
大神带着买彩票 台山市| 宿迁市| 托里县| 江油市| 炉霍县| 玉林市| 青浦区| 资讯| 长武县| 客服| 喜德县| 屯昌县| 平乐县| 许昌市| 区。| 襄垣县| 大关县| 馆陶县| 瑞昌市| 北票市| 碌曲县| 海伦市| 新建县| 屏南县| 定陶县| 十堰市| 扶绥县| 莲花县| 屯昌县| 常德市| 韩城市| 呼玛县| 宁武县| 安庆市| 宜宾县| 高碑店市| 旅游| 泗水县|