源起
在過(guò)去的近十年時(shí)間里,面向?qū)ο缶幊檀笮衅涞榔薄TS多企業(yè)級(jí)的應(yīng)用都是基于面向過(guò)程和面向?qū)ο髢煞N編程模型實(shí)現(xiàn)善延。日前,接觸了Python語(yǔ)言幻妓,學(xué)習(xí)了Python語(yǔ)言中的函數(shù)式編程,讓我對(duì)編程模式有了全新的認(rèn)識(shí)劫拢,故寫下此文肉津,與大家一起學(xué)習(xí)探討。
什么是函數(shù)式編程
在維基百科中舱沧,已經(jīng)對(duì)函數(shù)式編程有了詳細(xì)的介紹妹沙。
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
顧名思義,函數(shù)式編程是一種編程模型熟吏,它將計(jì)算機(jī)運(yùn)算看作是數(shù)學(xué)中函數(shù)的計(jì)算距糖,并且避免了狀態(tài)變化和可變數(shù)據(jù)元素。
一般地牵寺,編程語(yǔ)言分為命令式和聲明式兩類悍引。我們之前常使用的開(kāi)發(fā)語(yǔ)言諸如C,C++,Java都屬于命令式編程,諸如Lisp,Hashkell則是函數(shù)式編程的代表帽氓。時(shí)下比較流行的Python,Scala由于支持多種編程模式趣斤,既支持命令式編程,也支持函數(shù)式編程黎休;對(duì)于我們熟識(shí)的Java語(yǔ)言也在Java8開(kāi)始也支持了如lambda表達(dá)式等函數(shù)式編程的特性浓领。
在命令式編程中玉凯,通過(guò)一系列改變程序狀態(tài)的指令來(lái)完成計(jì)算,賦值語(yǔ)句占據(jù)著主導(dǎo)的地位联贩;相應(yīng)的漫仆,函數(shù)式編程則是通過(guò)數(shù)學(xué)函數(shù)的表達(dá)式變換和計(jì)算來(lái)求值,函數(shù)具有至高無(wú)上的地位泪幌。
函數(shù)式編程的意義
函數(shù)式編程到底有什么好處盲厌,讓眾多開(kāi)發(fā)者如此趨之如騖?
- 首先座菠,函數(shù)式編程更加模塊化狸眼,其定義了更加有用,容易重用浴滴,組合以及測(cè)試的抽象拓萌,使得代碼簡(jiǎn)潔,開(kāi)發(fā)快速升略,大大降低開(kāi)發(fā)成本微王。
- 其次,易于并發(fā)編程品嚣。由于函數(shù)式編程不修改變量炕倘,所以不存在“鎖”線程的問(wèn)題,不需要考慮死鎖(deadlock)翰撑。
- 再者罩旋,函數(shù)式編程回歸簡(jiǎn)單。如對(duì)表達(dá)式(1+2)*3-4眶诈,函數(shù)式方式可以通過(guò)add(1,2).multiply(3).subtract(4)來(lái)實(shí)現(xiàn)涨醋,更接近自然語(yǔ)言,易于理解逝撬。
- 更進(jìn)一步說(shuō)浴骂,函數(shù)式編程易于測(cè)試。由于函數(shù)式編程的每一個(gè)符號(hào)都是final的宪潮,沒(méi)有函數(shù)會(huì)產(chǎn)生副作用溯警。因?yàn)闆](méi)有在某個(gè)地方修改過(guò)值,也就沒(méi)有函數(shù)修改過(guò)在其作用域之外的量并被其他函數(shù)使用(如類成員或全局變量)狡相。因此梯轻,對(duì)于被測(cè)試程序中的每個(gè)函數(shù),你只需在意其參數(shù)谣光,而不必考慮函數(shù)的條用順序檩淋,索要做的就是傳遞代表了邊際情況的參數(shù)。
- 最后萄金,支持代碼熱部署蟀悦。對(duì)于函數(shù)式的程序,所有的狀態(tài)即傳遞給函數(shù)的參數(shù)都被保存在堆棧上氧敢,只要保證接口不變日戈,內(nèi)部實(shí)現(xiàn)是與外部無(wú)關(guān)的,這使得對(duì)代碼進(jìn)行熱部署成為可能孙乖。
幾個(gè)特征
高階函數(shù)
如果說(shuō)對(duì)象是在面向?qū)ο缶幊讨械幕A(chǔ)浙炼,那么函數(shù)則是函數(shù)式編程的第一塊磚頭。在面向?qū)ο缶幊讨形ò溃覀儼褜?duì)象傳來(lái)傳去弯屈,那在函數(shù)式編程中,我們要做的就是把函數(shù)傳來(lái)傳去恋拷,于是就產(chǎn)生了高階函數(shù)资厉。
對(duì)于高階函數(shù),我們作如下定義:
在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中蔬顾,高階函數(shù)式是至少滿足了下列一個(gè)條件的函數(shù):
1. 接收一個(gè)或多個(gè)函數(shù)作為輸入
2. 輸出一個(gè)函數(shù)
比如對(duì)數(shù)組[1,2,3,4]中所有的元素都進(jìn)行值翻倍處理宴偿,對(duì)于命令式編程來(lái)說(shuō)(以C實(shí)現(xiàn)為例)
int[] numbers = {1,2,3,4};
for(int i = 0;i < numbers.length;i++){
numbers[i] = numbers[i] * 2;
}
相應(yīng)的,如果是使用函數(shù)式編程(以python實(shí)現(xiàn)為例)
def doubling (x){
return 2 * x;
}
s = map(doubling ,[1,2,3,4])
二者打印得到的結(jié)果是一樣的诀豁。相比而言窄刘,使用函數(shù)式編程的代碼更簡(jiǎn)潔。在這其中舷胜,函數(shù)doubing作為函數(shù)map參數(shù)的進(jìn)行輸入娩践,滿足高階函數(shù)定義的條件一。
Lambda表達(dá)式
Lambda表達(dá)式是基于數(shù)學(xué)中的λ演算得名烹骨,直接對(duì)應(yīng)于其中的lambda抽象(lambda abstraction)翻伺,是一個(gè)匿名函數(shù)。在2014年發(fā)布Java8也包含了此項(xiàng)特性展氓。使用lambda表達(dá)式對(duì)上述例子進(jìn)行運(yùn)算穆趴,可以做到一行代碼完成。
s = map(lambda x: x * 2,[1,2,3,4])遇汞、
使用lambda表達(dá)式的好處就是減少代碼量未妹,使程序更加靈活易讀。
無(wú)狀態(tài)性
必須意識(shí)到空入,我們的程序是擁有“狀態(tài)”的络它。試想一下,在我們調(diào)試Java程序時(shí)歪赢,經(jīng)常會(huì)使用到添加斷點(diǎn)進(jìn)行單步跟蹤化戳。程序在執(zhí)行斷點(diǎn)就暫停,這個(gè)時(shí)候可以認(rèn)為程序停留在某一個(gè)狀態(tài)上。在這個(gè)狀態(tài)上保留了當(dāng)前定義的全部變量点楼。命令式編程是通過(guò)修改變量的值來(lái)保存當(dāng)前程序的狀態(tài)的扫尖。
而函數(shù)式編程不一樣,它是通過(guò)函數(shù)來(lái)保存程序的狀態(tài)的掠廓,或者更準(zhǔn)確一點(diǎn)换怖,它是通過(guò)函數(shù)創(chuàng)建新的參數(shù)或返回值來(lái)保存程序的狀態(tài)的。函數(shù)一層層的疊加起來(lái)蟀瞧,其中每個(gè)函數(shù)的參數(shù)或者返回結(jié)果來(lái)代表程序的一個(gè)中間狀態(tài)沉颂,過(guò)程式編程中對(duì)變量的修改在這里變成了一次函數(shù)轉(zhuǎn)換(一層函數(shù)的疊加)。這時(shí)悦污,我們會(huì)發(fā)現(xiàn)铸屉,無(wú)論多少個(gè)進(jìn)程在跑,因?yàn)楸旧頍o(wú)賦值操作切端,所以不會(huì)影響到我們的最終結(jié)果彻坛。
柯里化
柯里化(英語(yǔ):Currying),把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)的函數(shù)帆赢,返回接受余下的參數(shù)并且返回結(jié)果的新函數(shù)小压。
簡(jiǎn)單來(lái)說(shuō),柯里化就是一個(gè)函數(shù)在參數(shù)沒(méi)給全時(shí)返回另一個(gè)函數(shù)椰于,返回的函數(shù)的參數(shù)正好是余下的參數(shù)怠益。
比如:如果你設(shè)定了x,y兩個(gè)參數(shù),那么2的4次方返回的就是16,但是如果你只是指定了x為2瘾婿,而沒(méi)有指定y,那么就會(huì)返回一個(gè)函數(shù):2的y次方蜻牢,這個(gè)函數(shù)就只有一個(gè)參數(shù)y;
柯里化的好處是減少了函數(shù)的參數(shù)個(gè)數(shù),并且模塊化了每步計(jì)算偏陪,與設(shè)計(jì)模式中的適配器模式(將一個(gè)接口轉(zhuǎn)換為另一個(gè)接口)類似抢呆,并且柯里化的應(yīng)用之一"惰性求值"也是函數(shù)式編程的一個(gè)重要特性
閉包
如果你是使用javascript語(yǔ)言進(jìn)行日常開(kāi)發(fā),那么閉包對(duì)你來(lái)說(shuō)是個(gè)再熟悉不過(guò)的事物笛谦。
首先抱虐,來(lái)看看閉包的概念:閉包(Closure)是詞法閉包(Lexical Closure)的簡(jiǎn)稱,是引用了自由變量的函數(shù)饥脑。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在恳邀,即使已經(jīng)離開(kāi)了創(chuàng)造它的環(huán)境也不例外。所以灶轰,閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體谣沸。
下面用python語(yǔ)言的代碼來(lái)舉個(gè)例子:
def f1(prefix):
def f2(name):
print prefix, name
return f2
f = f1(1)
f(2)
f(3)
輸出的結(jié)果為:
1 2
1 3
可以看到,函數(shù)f2()訪問(wèn)了非本地變量prefix,這是正常的笋颤,但是為什么變量prefix在結(jié)束第一次函數(shù)運(yùn)行之后還會(huì)生效乳附,也就是輸出 1 2之后,在執(zhí)行下一次訪問(wèn)時(shí),prefix還是有效的赋除?
在Python語(yǔ)言對(duì)閉包原理的實(shí)現(xiàn)中阱缓,當(dāng)內(nèi)嵌函數(shù)引用了包含它的函數(shù)中的變量后(此處為函數(shù)f2引用了函數(shù)f1傳入的參數(shù)prefix),這些變量會(huì)被保存在f1的__closure__屬性中,成為f1的一部分贤重,因此變量prefix會(huì)和f1存在一致的生命周期茬祷,也就是上文提到的清焕,被引用的自有變量將和該函數(shù)一同存在并蝗,即使已經(jīng)離開(kāi)了創(chuàng)造它的環(huán)境。
寫在最后
函數(shù)式編程秸妥,如文章開(kāi)頭所言滚停,是一種編程范式,并不局限在如Lisp,Hashkell這類函數(shù)式語(yǔ)言中才能使用≈嗑澹現(xiàn)代的開(kāi)發(fā)語(yǔ)言很多都支持多模式键畴,只要具備一定的特性,就可以用來(lái)編寫函數(shù)式程序突雪,如python,scala等起惕。編程語(yǔ)言的流行程度與其擅長(zhǎng)的領(lǐng)域密切相關(guān)。函數(shù)式語(yǔ)言長(zhǎng)于數(shù)理邏輯以及并行程序咏删。命令式則擅于業(yè)務(wù)邏輯惹想,尤其是交互式或事件驅(qū)動(dòng)上。選擇何種語(yǔ)言督函,采用何種模式來(lái)實(shí)現(xiàn)嘀粱,需要密切關(guān)聯(lián)實(shí)際業(yè)務(wù)需求做出最終的抉擇。就像如果要用scala完全取代了今天的Java工作辰狡,我想恐怕效果會(huì)很槽糕锋叨,而如果使用scala來(lái)負(fù)責(zé)底層服務(wù)的編寫,恐怕就比Java語(yǔ)言適合得多了宛篇。