誕生50多年以後,函數式編程(functional programming)開始得到愈來愈多的關注。java
不只最古老的函數式語言Lisp重獲青春,並且新的函數式語言層出不窮,好比Erlang、clojure、Scala、F#等等。目前最當紅的Python、Ruby、Javascript,對函數式編程的支持都很強,就連老牌的面向對象的Java、面向過程的PHP,都忙不迭地加入對匿名函數的支持。愈來愈多的跡象代表,函數式編程已經再也不是學術界的最愛,開始大踏步地在業界投入實用。程序員
也許繼"面向對象編程"以後,"函數式編程"會成爲下一個編程的主流範式(paradigm)。將來的程序員恐怕或多或少都必須懂一點。express
可是,"函數式編程"看上去比較難,缺少通俗的入門教程,各類介紹文章都充斥着數學符號和專用術語,讓人讀了如墜雲霧。就連最基本的問題"什麼是函數式編程",網上都搜不到易懂的回答。編程
下面是個人"函數式編程"學習筆記,分享出來,與你們一塊兒探討。內容不涉及數學(我也不懂Lambda Calculus),也不涉及高級特性(好比lazy evaluation和currying),只求儘可能簡單通俗地整理和表達,我如今所理解的"函數式編程"以及它的意義。併發
我主要參考了Slava Akhmechet的"Functional Programming For The Rest of Us"。ide
1、定義模塊化
簡單說,"函數式編程"是一種"編程範式"(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);
這就是函數式編程。
2、特色
函數式編程具備五個鮮明的特色。
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),指的是函數的運行不依賴於外部變量或"狀態",只依賴於輸入的參數,任什麼時候候只要參數相同,引用函數所獲得的返回值老是相同的。
有了前面的第三點和第四點,這點是很顯然的。其餘類型的語言,函數的返回值每每與系統狀態有關,不一樣的狀態之下,返回值是不同的。這就叫"引用不透明",很不利於觀察和理解程序的行爲。
3、意義
函數式編程到底有什麼好處,爲何會變得愈來愈流行?
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語言早就證實了這一點,它是瑞典愛立信公司爲了管理電話系統而開發的,電話系統的升級固然是不能停機的