誕生50多年之后,函數式編程(functional programming)開(kāi)始獲得越來(lái)越多的關(guān)注。
不僅最古老的函數式語(yǔ)言L(fǎng)isp重獲青春,而且新的函數式語(yǔ)言層出不窮,比如Erlang、clojure、Scala,、F#等等。目前最當紅的Python、Ruby、Javascript,對函數式編程的支持都很強,就連老牌的面向對象的Java、面向過(guò)程的PHP,都忙不迭地加入對匿名函數的支持。越來(lái)越多的跡象表明,函數式編程已經(jīng)不再是學(xué)術(shù)界的最?lèi)?ài),開(kāi)始大踏步地在業(yè)界投入實(shí)用。
也許繼"面向對象編程"之后,"函數式編程"會(huì )成為下一個(gè)編程的主流范式(paradigm)。未來(lái)的程序員恐怕或多或少都必須懂一點(diǎn)。

但是,"函數式編程"看上去比較難,缺乏通俗的入門(mén)教程,各種介紹文章都充斥著(zhù)數學(xué)符號和專(zhuān)用術(shù)語(yǔ),讓人讀了如墜云霧。就連最基本的問(wèn)題"什么是函數式編程",網(wǎng)上都搜不到易懂的回答。
下面是我的"函數式編程"學(xué)習筆記,分享出來(lái),與大家一起探討。內容不涉及數學(xué)(Lambda Calculus我也不懂),也不涉及高級特性(比如lazy evaluation和currying),只求盡量簡(jiǎn)單通俗地整理和表達,我現在所理解的"函數式編程"以及它的意義。
我主要參考了Slava Akhmechet的"Functional Programming For The Rest of Us"。
一、定義
簡(jiǎn)單說(shuō),"函數式編程"是一種"編程范式"(programming paradigm),也就是如何編寫(xiě)程序的方法論。
它屬于"結構化編程"的一種,主要思想是把運算過(guò)程盡量寫(xiě)成一系列嵌套的函數調用。舉例來(lái)說(shuō),現在有這樣一個(gè)數學(xué)表達式:
(1 + 2) * 3 - 4
傳統的過(guò)程式編程,可能這樣寫(xiě):
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函數式編程要求使用函數,我們可以把運算過(guò)程定義為不同的函數,然后寫(xiě)成下面這樣:
var result = subtract(multiply(add(1,2), 3), 4);
這就是函數式編程。
二、特點(diǎn)
函數式編程具有五個(gè)鮮明的特點(diǎn)。
1. 函數是"第一等公民"
所謂"第一等公民"(first class),指的是函數與其他數據類(lèi)型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個(gè)函數,或者作為別的函數的返回值。
舉例來(lái)說(shuō),下面代碼中的print變量就是一個(gè)函數,可以作為另一個(gè)函數的參數。
var print = function(i){ console.log(i);};
[1,2,3].forEach(print);
2. 只用"表達式",不用"語(yǔ)句"
"表達式"(expression)是一個(gè)單純的運算過(guò)程,總是有返回值;"語(yǔ)句"(statement)是執行某種操作,沒(méi)有返回值。函數式編程要求,只使用表達式,不使用語(yǔ)句。也就是說(shuō),每一步都是單純的運算,而且都有返回值。
原因是函數式編程的開(kāi)發(fā)動(dòng)機,一開(kāi)始就是為了處理運算(computation),不考慮系統的讀寫(xiě)(I/O)。"語(yǔ)句"屬于對系統的讀寫(xiě)操作,所以就被排斥在外。
當然,實(shí)際應用中,不做I/O是不可能的。因此,編程過(guò)程中,函數式編程只要求把I/O限制到最小,不要有不必要的讀寫(xiě)行為,保持計算過(guò)程的單純性。
3. 沒(méi)有"副作用"
所謂"副作用"(side effect),指的是函數內部與外部互動(dòng)(最典型的情況,就是修改全局變量的值),產(chǎn)生運算以外的其他結果。
函數式編程強調沒(méi)有"副作用",意味著(zhù)函數要保持獨立,所有功能就是返回一個(gè)新的值,沒(méi)有其他行為,尤其是不得修改外部變量的值。
4. 不修改狀態(tài)
上一點(diǎn)已經(jīng)提到,函數式編程只是返回新的值,不修改系統變量。因此,不修改變量,也是它的一個(gè)重要特點(diǎn)。
在其他類(lèi)型的語(yǔ)言中,變量往往用來(lái)保存"狀態(tài)"(state)。不修改變量,意味著(zhù)狀態(tài)不能保存在變量中。函數式編程使用參數保存狀態(tài),最好的例子就是遞歸。下面的代碼是一個(gè)將字符串逆序排列的函數,它演示了不同的參數如何決定了運算所處的"狀態(tài)"。
function reverse(string) {
if(string.length == 0) {
return string;
} else {
return reverse(string.substring(1, string.length)) + string.substring(0, 1);
}
}
由于使用了遞歸,函數式語(yǔ)言的運行速度比較慢,這是它長(cháng)期不能在業(yè)界推廣的主要原因。
5. 引用透明
引用透明(Referential transparency),指的是函數的運行不依賴(lài)于外部變量或"狀態(tài)",只依賴(lài)于輸入的參數,任何時(shí)候只要參數相同,引用函數所得到的返回值總是相同的。
有了前面的第三點(diǎn)和第四點(diǎn),這點(diǎn)是很顯然的。其他類(lèi)型的語(yǔ)言,函數的返回值往往與系統狀態(tài)有關(guān),不同的狀態(tài)之下,返回值是不一樣的。這就叫"引用不透明",很不利于觀(guān)察和理解程序的行為。
三、意義
函數式編程到底有什么好處,為什么會(huì )變得越來(lái)越流行?
1. 代碼簡(jiǎn)潔,開(kāi)發(fā)快速
函數式編程大量使用函數,減少了代碼的重復,因此程序比較短,開(kāi)發(fā)速度較快。
Paul Graham在《黑客與畫(huà)家》一書(shū)中寫(xiě)道:同樣功能的程序,極端情況下,Lisp代碼的長(cháng)度可能是C代碼的二十分之一。
如果程序員每天所寫(xiě)的代碼行數度基本相同,這就意味著(zhù),"C語(yǔ)言需要一年時(shí)間完成開(kāi)發(fā)某個(gè)功能,Lisp語(yǔ)言只需要不到三星期。反過(guò)來(lái)說(shuō),如果某個(gè)新功能,Lisp語(yǔ)言完成開(kāi)發(fā)需要三個(gè)月,C語(yǔ)言需要寫(xiě)五年。"當然,這樣的對比故意夸大了差異,但是"在一個(gè)高度競爭的市場(chǎng)中,即使開(kāi)發(fā)速度只相差兩三倍,也足以使得你永遠處在落后的位置。"
2. 接近自然語(yǔ)言,易于理解
函數式編程的自由度很高,可以寫(xiě)出很接近自然語(yǔ)言的代碼。
前文曾經(jīng)將表達式(1 + 2) * 3 - 4,寫(xiě)成函數式語(yǔ)言:
subtract(multiply(add(1,2), 3), 4)
對它進(jìn)行變形,不難得到另一種寫(xiě)法:
add(1,2).multiply(3).subtract(4)
這基本就是自然語(yǔ)言的表達了。再看下面的代碼,大家應該一眼就能明白它的意思吧:
merge([1,2],[3,4]).sort().search("2")
因此,函數式編程的代碼更容易理解。
3. 更方便的代碼管理
函數式編程不依賴(lài)、也不會(huì )改變外界的狀態(tài),只要給定輸入參數,返回的結果必定相同。因此,每一個(gè)函數都可以被看做獨立單元,很有利于進(jìn)行單元測試(unit testing)和除錯(debugging),以及模塊化組合。
4. 易于"并發(fā)編程"
函數式編程不需要考慮"死鎖"(deadlock),因為它不修改變量,所以根本不存在"鎖"線(xiàn)程的問(wèn)題。不必擔心一個(gè)線(xiàn)程的數據,被另一個(gè)線(xiàn)程修改,所以可以很放心地把工作分攤到多個(gè)線(xiàn)程,部署"并發(fā)編程"(concurrency)。
請看下面的代碼:
var s1 = Op1();
var s2 = Op2();
var s3 = concat(s1, s2);
由于s1和s2互不干擾,不會(huì )修改變量,誰(shuí)先執行是無(wú)所謂的,所以可以放心地增加線(xiàn)程,把它們分配在兩個(gè)線(xiàn)程上完成。其他類(lèi)型的語(yǔ)言就做不到這一點(diǎn),因為s1可能會(huì )修改系統狀態(tài),而s2可能會(huì )用到這些狀態(tài),所以必須保證s2在s1之后運行,自然也就不能部署到其他線(xiàn)程上了。
多核CPU是將來(lái)的潮流,所以函數式編程的這個(gè)特性非常重要。
5. 代碼的熱升級
函數式編程沒(méi)有副作用,只要保證接口不變,內部實(shí)現是外部無(wú)關(guān)的。所以,可以在運行狀態(tài)下直接升級代碼,不需要重啟,也不需要停機。Erlang語(yǔ)言早就證明了這一點(diǎn),它是瑞典愛(ài)立信公司為了管理電話(huà)系統而開(kāi)發(fā)的,電話(huà)系統的升級當然是不能停機的。
聯(lián)系客服