
第一步先來理解函數(shù)式編程的概念,這是最重要,也往往是最難的一步。但是完全不必如此。因?yàn)槟愕囊暯遣粚?duì)。
學(xué)習(xí)開車

當(dāng)我們第一次學(xué)習(xí)開車的時(shí)候,我們也掙扎過。沒錯(cuò),看著別人開車是如此的簡(jiǎn)單。但事實(shí)證明比我們想象的要難。
我們?cè)谧约焊改傅能嚴(yán)锞毩?xí),在熟悉我們自己街區(qū)的街道之前也不敢冒險(xiǎn)上高速路。
但是通過反復(fù)練習(xí),經(jīng)歷一些我們父母愿意忘記的驚恐時(shí)刻后,我們學(xué)會(huì)了駕駛并最終獲得了駕照。
駕照在手,我們可以在任意可能的時(shí)刻開車出去。一次次的旅行,我們的技術(shù)越來越好,信心越來越高。然后某一天,我們需要駕駛別人的汽車或者是已有的汽車報(bào)廢了我們需要買一輛新的。
第一次駕駛另外一輛車是什么感覺?和第一次駕駛汽車的感覺一樣嗎?差的還不是一點(diǎn)的遠(yuǎn)。第一次是如此陌生。在那之前我們也坐過汽車,但是僅僅是作為一名乘客。這一次我們是坐在駕駛座。擁有絕對(duì)的掌控權(quán)。
但是當(dāng)我們駕駛我們的第二輛汽車時(shí),我們通常會(huì)問自己幾個(gè)簡(jiǎn)單的問題比如:鑰匙孔在哪里,燈光開關(guān)在哪里,怎樣使用轉(zhuǎn)向燈和怎樣調(diào)節(jié)后視鏡。
然后就可以平穩(wěn)的行使。但是為什么這一次和第一次相比如此容易?
那是因?yàn)樾碌钠嚭鸵郧暗钠嚪浅O嗨?。它擁有所有和其他汽車一樣的必備基礎(chǔ)設(shè)施,并且?guī)缀醵荚谕粋€(gè)位置。
它有一小部分東西可能通過不同的方式實(shí)現(xiàn),也有可能擁有一些附加功能。但是我們?cè)诘谝淮务{駛時(shí)不會(huì)使用這些功能,甚至第二次駕駛時(shí)也不會(huì)使用。最終,我們會(huì)學(xué)會(huì)使用這些新功能。至少學(xué)會(huì)我們關(guān)心的那些。
沒錯(cuò),學(xué)習(xí)編程語言和這個(gè)過程有點(diǎn)相似。第一個(gè)是最難的。一旦搞定了一個(gè),后面的就很簡(jiǎn)單。
當(dāng)你開始學(xué)習(xí)第二個(gè)語言的時(shí)候,你會(huì)問一些問題比如,“怎么創(chuàng)建一個(gè)模塊?怎么搜索一個(gè)數(shù)組?substring函數(shù)的參數(shù)是什么?”
你很自信能夠?qū)W會(huì)駕馭這門新語言,因?yàn)樗嵝涯?,已有的語言或許加上一些新的特性以后有望讓你的生活變的更簡(jiǎn)單。
你的第一艘宇宙飛船

不管你一生中是否駕駛過一輛汽車或者幾十輛汽車,想象一下你即將要駕駛一艘宇宙飛船。
如果你準(zhǔn)備去駕駛一艘宇宙飛船,你不要指望在馬路上的駕駛能力能夠幫上什么忙。你必須從0開始。(我們都是程序員。我們計(jì)數(shù)從0開始。)
你會(huì)預(yù)期太空中的一切都會(huì)不同,駕駛這個(gè)新奇裝置和在陸地上駕駛也會(huì)不同,并按照這個(gè)預(yù)期來培訓(xùn)自己。
物理特性并沒有改變。都只是你在同一個(gè)宇宙中導(dǎo)航的方式。
學(xué)習(xí)函數(shù)式編程也是如此。你應(yīng)該預(yù)想事情將會(huì)非常不同。并且很多你所了解的編程知識(shí)都不能轉(zhuǎn)換。
編程是需要思考的事情,函數(shù)式編程會(huì)教會(huì)你用完全不同的方式思考。所以,你可能永遠(yuǎn)不會(huì)回到以前的思維方式。
忘記你所知道的一切

人們總是喜歡說這句諺語,但是這是真的。學(xué)習(xí)函數(shù)式編程就像從0開始。不完全是,但是印象深刻。有許多相似的概念,但是你最好預(yù)設(shè)你必須重學(xué)所有的東西。
有了正確的視角,你就會(huì)有正確的預(yù)期。有了正確的預(yù)期才不會(huì)在事情變得艱難時(shí)退出。
有各種各樣作為程序員已經(jīng)習(xí)慣做的事情在函數(shù)式編程中都不能再做。
就像駕駛汽車一樣,你已經(jīng)習(xí)慣倒出私人車道。但是在宇宙飛船里沒有倒擋?,F(xiàn)在你也許會(huì)想:“什么?沒有倒擋?!沒有倒擋我還怎么駕駛?!”
好吧,事實(shí)證明宇宙飛船不需要倒擋,因?yàn)樘罩锌梢栽谌齻€(gè)維度運(yùn)動(dòng)。一旦你理解了這一點(diǎn),你就不會(huì)再想念倒擋。事實(shí)上某一天你會(huì)覺得汽車太過于限制。
學(xué)習(xí)函數(shù)式編程需要一點(diǎn)時(shí)間。所以請(qǐng)有點(diǎn)耐心。
讓我們一起走出冰冷的命令式編程世界,縱身一躍,跳進(jìn)到函數(shù)編程溫泉中吧。
接下來的一系列文章是一些函數(shù)式編程概念,這些概念在你學(xué)習(xí)你的第一個(gè)函數(shù)式語言之前非常有用。或許你已經(jīng)投身學(xué)習(xí)函數(shù)式編程,本文將幫助你理解。
請(qǐng)不要著急。從現(xiàn)在開始花時(shí)間閱讀并花時(shí)間去理解示例代碼。你也許想在閱讀完一節(jié)后停止閱讀并消化這些概念。稍后再回來完成剩余部分。
最重要的事情是你要理解。
純凈

當(dāng)函數(shù)式程序員在談到純凈時(shí),他們是指純函數(shù)。
純函數(shù)是非常簡(jiǎn)單的函數(shù)。他們僅僅針對(duì)它們的輸入?yún)?shù)進(jìn)行操作。
這里有一個(gè)JavaScript版本的純函數(shù)示例:
var z = 10;
function add(x, y) {
return x + y;
}
注意add函數(shù)沒有去碰變量z。它沒有讀取z的值,也沒有保存數(shù)據(jù)到z。它僅僅讀取x和y,也就是它的輸入?yún)?shù),然后返回兩者相加的結(jié)果。
這就是一個(gè)純函數(shù)。如果add 函數(shù)訪問了z,它就不再是一個(gè)純函數(shù)。
再來看另一個(gè)函數(shù):
function justTen() {
return 10;
}
如果justTen是一個(gè)純函數(shù),那么他只能返回一個(gè)常數(shù)。為什么?
因?yàn)槲覀儧]有給它任何輸入。作為純函數(shù),它不能訪問輸入?yún)?shù)以外的任何變量,它唯一能夠返回的就是一個(gè)常數(shù)。
由于沒有輸入?yún)?shù)的純函數(shù)不能做任何事情,它們是沒有實(shí)際用途的。更好的做法是將justTen定義成一個(gè)常量。
大多數(shù)有用的純函數(shù)都應(yīng)有至少一個(gè)參數(shù)。
來看一下這個(gè)函數(shù):
function addNoReturn(x, y) {
var z = x + y
}
注意這個(gè)函數(shù)沒有返回值。它將 x 和 y 加起來并賦值給變量z但是沒有返回。
它是一個(gè)純函數(shù)因?yàn)樗鼉H僅處理其輸入?yún)?shù)。它做了加法運(yùn)算,但是由于它不返回任何值,它是無用的。
所有有用的純函數(shù)都應(yīng)該返回一些東西。
我們?cè)賮砜匆幌碌谝粋€(gè)add函數(shù):
function add(x, y) {
return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3
注意add(1,2)的結(jié)果總是 3。不是多么驚奇的事情因?yàn)檫@是一個(gè)純函數(shù)。如果add函數(shù)使用了外部的值,你根本不可能預(yù)測(cè)它的行為。
純函數(shù)對(duì)于給定相同的輸入,總是產(chǎn)生相同的輸出。
由于純函數(shù)不能修改任何外部變量,以下所有的函數(shù)都是不純的:
writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);
所有這些函數(shù)都有所謂的函數(shù)副作用。 當(dāng)你調(diào)用它們的時(shí)候,它們會(huì)修改文件和數(shù)據(jù)庫表,發(fā)送數(shù)據(jù)到服務(wù)器或者調(diào)用操作系統(tǒng)接口獲取一個(gè)socket。它們所做的事情遠(yuǎn)遠(yuǎn)多于僅僅操作輸入?yún)?shù)并返回其輸出。所以你不可能預(yù)測(cè)這些函數(shù)會(huì)返回什么。
純函數(shù)沒有函數(shù)副作用。
在命令式編程語言中,比如JavaScript,Java和C#,函數(shù)副作用到處都是。這使得調(diào)試非常困難,因?yàn)橐粋€(gè)變量在你程序中的任意一個(gè)地方都可能被修改。所以當(dāng)因?yàn)橐粋€(gè)變量在錯(cuò)誤的時(shí)間被修改成一個(gè)錯(cuò)誤值而引發(fā)bug時(shí),你在哪里查找這個(gè)bug?到處查找?這不太好。
現(xiàn)在你也許正在想,“只有純函數(shù)我還怎么做事情?!”
在函數(shù)式編程中,你不僅僅編寫純函數(shù)。
函數(shù)式語言不能消除函數(shù)副作用,它們只能限制函數(shù)副作用。因?yàn)槌绦虮仨毢驼鎸?shí)世界交互,每一個(gè)程序總有一些部分必須是不純的。目標(biāo)是減少不純代碼的數(shù)量并將它們和我們程序中的其他部分隔離。
不可變性

你還記得你第一次看到這樣的代碼是什么時(shí)候嗎:
var x = 1
x = x + 1
是誰教你忘記你在數(shù)學(xué)課堂上學(xué)到的內(nèi)容?在數(shù)學(xué)中,x永遠(yuǎn)不可能等于x + 1。
但是在命令式編程中,它的意思是:獲取當(dāng)前x的值然后加 1并將結(jié)果放回到x中。
那么,在函數(shù)式編程中x = x + 1是非法的。所以你必須記起來一些你在數(shù)學(xué)課堂學(xué)會(huì)但是已經(jīng)忘記的內(nèi)容。
在函數(shù)式編程中沒有變量。
由于歷史原因,已經(jīng)保存的值仍然稱為變量,但是它們是常量,即:一旦x被賦予了一個(gè)值,它終身都是那個(gè)值。
不用擔(dān)心,x通常是一個(gè)局部變量,其生命周期通常都很短。但是只要它還活著,他的值就不能被修改。
這里有一個(gè)Elm版本的常量示例,Elm是一個(gè)用于Web開發(fā)的純函數(shù)式語言:
addOneToSum y z =
let
x = 1
in
x + y + z
如果你對(duì)ML-Style句法不熟悉,我來解釋一下:addOnetoSum是一個(gè)函數(shù),它有兩個(gè)輸入?yún)?shù)y 和z。
在let代碼塊中,x的值被綁定為1,即它剩下的生命周期中,它的值總是1。它的生命周期在這個(gè)函數(shù)退出時(shí)就結(jié)束了,或者更準(zhǔn)確的說當(dāng)let代碼塊被評(píng)估完時(shí)就結(jié)束了。
在 in代碼塊中,加法運(yùn)算可以包含 let塊中定義好的值,即x。x + y + z的運(yùn)算結(jié)果被返回,更準(zhǔn)確的說是1 + y + z的運(yùn)算結(jié)果被返回因?yàn)?strong>x = 1。
再一次我聽見你在問:“沒有變量我還怎么做事情?!”
我們來想一下什么時(shí)候需要修改變量。有兩種常用的情況:多值修改(例如:修改一個(gè)對(duì)象或者記錄的一個(gè)值)和單值修改(例如:循環(huán)計(jì)數(shù)器)。
函數(shù)式編程通過記錄處理變量修改,拷貝一份被修改的值的記錄。它通過一定的數(shù)據(jù)結(jié)構(gòu)使得高效處理變得可能,通過一定的數(shù)據(jù)接口不需要拷貝記錄中的所有數(shù)據(jù)。
函數(shù)式編程在處理單值修改時(shí)使用完全相同的方式,拷貝。
噢,是的,沒有循環(huán)。
“什么,沒有變量現(xiàn)在沒有循環(huán)???!我恨你?。?!”
等等,并不是說我們不能做循環(huán)(沒有別的意思),僅僅是沒有特殊的循環(huán)結(jié)構(gòu),比如for, while, do, repeat等等。
函數(shù)式編程通過遞歸實(shí)現(xiàn)循環(huán)。
在JavaScript中有兩種方式你可以實(shí)現(xiàn)循環(huán):
// simple loop construct
var acc = 0;
for (var i = 1; i <= 10; ++i)
acc += i;
console.log(acc); // prints 55
// without loop construct or variables (recursion)
function sumRange(start, end, acc) {
if (start > end)
return acc;
return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // prints 55
注意函數(shù)式方法中,遞歸是怎樣通過調(diào)用自己時(shí)使用新的起始位置(start + 1) 和新的累加結(jié)果(acc + start)達(dá)到和for循環(huán)相同的效果的。它沒有修改舊的值。取而代之的是他使用了舊值的計(jì)算結(jié)果。
不幸的是,即使你花點(diǎn)時(shí)間調(diào)查一下也很難在JavaScript中找到這樣的實(shí)現(xiàn),原因有兩點(diǎn)。第一,JavaScript的句法太嘈雜,第二,你可能不習(xí)慣遞歸的思考方式。
在Elm中,閱讀和理解起來就要容易一些:
sumRange start end acc =
if start > end then
acc
else
sumRange (start + 1) end (acc + start)
這是它的運(yùn)行過程:
sumRange 1 10 0 = -- sumRange (1 + 1) 10 (0 + 1)
sumRange 2 10 1 = -- sumRange (2 + 1) 10 (1 + 2)
sumRange 3 10 3 = -- sumRange (3 + 1) 10 (3 + 3)
sumRange 4 10 6 = -- sumRange (4 + 1) 10 (6 + 4)
sumRange 5 10 10 = -- sumRange (5 + 1) 10 (10 + 5)
sumRange 6 10 15 = -- sumRange (6 + 1) 10 (15 + 6)
sumRange 7 10 21 = -- sumRange (7 + 1) 10 (21 + 7)
sumRange 8 10 28 = -- sumRange (8 + 1) 10 (28 + 8)
sumRange 9 10 36 = -- sumRange (9 + 1) 10 (36 + 9)
sumRange 10 10 45 = -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 = -- 11 > 10 => 55
55
也許你覺得for循環(huán)更容易理解。這是一個(gè)有爭(zhēng)議的問題,并且更像是一個(gè)熟悉性的問題,沒有遞歸的循環(huán)需要可修改的能力(Mutability),這點(diǎn)不太好。
我還沒有完整的解釋不可修改性(Immutability)的好處, 可以閱讀Why Programmers Need Limits這篇文章中Global Mutable State這一節(jié)了更多信息。
一個(gè)明顯的好處就是如果你有權(quán)限訪問程序中某一個(gè)值,你只有讀取權(quán)限,也就意味著其他任何人都不能修改那個(gè)值。即使你也不可以。這樣就不會(huì)有偶發(fā)的修改。
此外,即使你的程序是多線程的,其他線程也不可能給你制造麻煩。由于該值是一個(gè)常量,其他線程如果想要修改那么它需要拷貝一份舊值。
回到90年代中期,我編寫了一個(gè)游戲引擎Creature Crunch,最大的bug來源就是多線程問題。我真希望那個(gè)時(shí)候就知道不變性(immutability)。但是那個(gè)時(shí)候我所擔(dān)心的游戲性能在2X或者4X的光驅(qū)上的差異。
不變性創(chuàng)建了更簡(jiǎn)單更安全的代碼
我的腦袋?。。?!
