誕生50多年之後,函數語言程式設計(functional programming)開始獲得越來越多的關注。
不僅最古老的函式式語言Lisp重獲青春,而且新的函式式語言層出不窮,比如Erlang、clojure、Scala、F#等等。目前最當紅的Python、Ruby、Javascript,對函數語言程式設計的支援都很強,就連老牌的物件導向的Java、程式導向的PHP,都忙不迭地加入對匿名函式的支援。越來越多的跡象表明,函數語言程式設計已經不再是學術界的最愛,開始大踏步地在業界投入實用。
也許繼"物件導向程式設計"之後,"函數語言程式設計"會成為下一個程式設計的主流正規化(paradigm)。未來的程式設計師恐怕或多或少都必須懂一點。
但是,"函數語言程式設計"看上去比較難,缺乏通俗的入門教程,各種介紹文章都充斥著數學符號和專用術語,讓人讀了如墜雲霧。就連最基本的問題"什麼是函數語言程式設計",網上都搜不到易懂的回答。
下面是我的"函數語言程式設計"學習筆記,分享出來,與大家一起探討。內容不涉及數學(我也不懂Lambda Calculus),也不涉及高階特性(比如lazy evaluation和currying),只求儘量簡單通俗地整理和表達,我現在所理解的"函數語言程式設計"以及它的意義。
我主要參考了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語言早就證明了這一點,它是瑞典愛立信公司為了管理電話系統而開發的,電話系統的升級當然是不能停機的。
(完)