最近在學(xué)習(xí)Erlang和Python。Erlang是完全的函數(shù)式編程語(yǔ)言,Python語(yǔ)言是面向?qū)ο蟮恼Z(yǔ)言纫骑,但是它的語(yǔ)法引入了大量的函數(shù)式編程思想。越研究越覺(jué)得函數(shù)式的編程思路可以幫助我們規(guī)避很多Bug珠插,所以在這里對(duì)函數(shù)式編程做一個(gè)簡(jiǎn)要的介紹惧磺。分析函數(shù)式編程的特點(diǎn)、方法論捻撑,使用的技術(shù)磨隘,以及同面向?qū)ο缶幊痰漠愅?/p>
?1缤底、函數(shù)式編程簡(jiǎn)介
背景
函數(shù)式編程誕生于50多年前。現(xiàn)在越來(lái)越多的人開(kāi)始接受并進(jìn)行函數(shù)式編程的實(shí)踐番捂。不僅最古老的函數(shù)式語(yǔ)言 Lisp 重獲青春个唧,而且新的函數(shù)式語(yǔ)言層出不窮,比如 Erlang设预、clojure徙歼、Scala、F#等等鳖枕。目前最當(dāng)紅的Objective-C, Python魄梯、Ruby、 Javascript都引入了對(duì)函數(shù)式編程的支持宾符。就連老牌的面向?qū)ο蟮?Java酿秸、面向過(guò)程的 PHP, 以及蘋(píng)果最新的swift語(yǔ)言,都忙不迭地加入匿名函數(shù)等機(jī)制魏烫。
越來(lái)越多的跡象表明辣苏,函數(shù)式編程已經(jīng)不再是學(xué)術(shù)界的最?lèi)?ài),開(kāi)始大踏步地在業(yè)界投入實(shí)用哄褒。 也許繼面向?qū)ο缶幊讨笙◇瘮?shù)式編程會(huì)成為下一個(gè)編程的主流范式。
說(shuō)到函數(shù)式編程編程就不得不說(shuō)面向?qū)ο竽派摹C嫦驅(qū)ο笫前岩粋€(gè)功能的一組操作和相關(guān)數(shù)據(jù)封裝在一個(gè)對(duì)象里退客,面向?qū)ο笫菍?duì)象滿(mǎn)天飛。函數(shù)式編程是把一個(gè)功能的一個(gè)操作和相關(guān)數(shù)據(jù)封裝在一起链嘀,函數(shù)式編程是函數(shù)滿(mǎn)天飛井辜。函數(shù)式編程比面向?qū)ο蟮膬?yōu)勢(shì)就是粒度更小,生命周期更短管闷。減少bug的有效途徑就是減少變量的生命周期粥脚,縮小模塊的粒度;所以函數(shù)式編程更不容易引入bug包个。
定義
是一種編程范型刷允,它將計(jì)算機(jī)運(yùn)算視為數(shù)學(xué)上的函數(shù)計(jì)算,并且避免使用程序狀態(tài)以及易變對(duì)象碧囊。函數(shù)編程語(yǔ)言最重要的基礎(chǔ)是λ演算(lambda calculus)树灶。λ演算中最關(guān)鍵的要素就是函數(shù)被當(dāng)作變量處理,能夠參與運(yùn)算糯而。
價(jià)值觀
函數(shù)式編程強(qiáng)調(diào)程序的執(zhí)行結(jié)果比執(zhí)行過(guò)程更重要天通。關(guān)注于描述問(wèn)題,而不是怎么實(shí)現(xiàn)熄驼,隱藏實(shí)現(xiàn)細(xì)節(jié)像寒。
化繁為簡(jiǎn)烘豹。利用若干簡(jiǎn)單的執(zhí)行單元讓計(jì)算結(jié)果不斷漸進(jìn),逐層推導(dǎo)復(fù)雜的運(yùn)算诺祸。
?你的優(yōu)秀和我的人生無(wú)關(guān)携悯,請(qǐng)帶著你的趾高氣揚(yáng)滾蛋吧。
下面是一個(gè)輸出數(shù)組的例子
shoplist = ['apple','mango','carrot','banana']print'My shopping list is now', shoplist#輸出#My shopping list is now ['banana', 'carrot', 'mango', 'rice']
這樣的代碼更易讀筷笨,代碼只是描述在干什么憔鬼,而不是如何做到這點(diǎn)的具體實(shí)現(xiàn)。如果是過(guò)程式編程胃夏,需要一個(gè)for循環(huán)去描述實(shí)現(xiàn)細(xì)節(jié)轴或。
函數(shù)式編程在解決復(fù)雜運(yùn)算問(wèn)題時(shí),把一個(gè)問(wèn)題分解為若干子問(wèn)題仰禀,逐步求解侮叮。軟件或程序的拼裝會(huì)變得更為簡(jiǎn)單和直觀。使代碼更容易理解悼瘾,方便排查問(wèn)題,并且具有更好的可維護(hù)性和擴(kuò)展性审胸。
現(xiàn)在有這樣一個(gè)數(shù)學(xué)表達(dá)式:
(1 + 2) * 3 - 4
傳統(tǒng)的過(guò)程式編程:
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函數(shù)式編程要求使用函數(shù),我們可以把運(yùn)算過(guò)程定義為不同的函數(shù),然后寫(xiě)成下面這樣:
var result = subtract(multiply(add(1,2), 3), 4);
你的優(yōu)秀和我的人生無(wú)關(guān)亥宿,請(qǐng)帶著你的趾高氣揚(yáng)滾蛋吧。這個(gè)就是函數(shù)式編程的準(zhǔn)則:函數(shù)不受外部變量影響砂沛,不依賴(lài)于外部變量烫扼,也不改變外部變量的值。
傳統(tǒng)的過(guò)程式編程:
int count;void increment()
{
? ? returen count++;
}
函數(shù)式編程:
def increment(count):
? returncount+1;
函數(shù)不訪問(wèn)全局變量碍庵,也不改變?nèi)肿兞俊?/p>
2映企、函數(shù)式編程特性
? 封裝、繼承静浴、多態(tài)是面向?qū)ο缶幊痰娜筇匦匝呙ァ:瘮?shù)式編程也有自己的語(yǔ)言特性。
數(shù)據(jù)不可變性(immutable data)多有的變量只可以賦值一次苹享,變量不可變双絮,如果想改變變量就創(chuàng)建一個(gè)新的變量。
數(shù)據(jù)的不可變性保證了程序是無(wú)狀態(tài)的得问,很多難解的bug往往是由各種復(fù)雜的狀態(tài)引起的囤攀。比如發(fā)現(xiàn)某些情況下程序運(yùn)行有問(wèn)題,是某一個(gè)狀態(tài)引起的宫纬,但是這個(gè)狀態(tài)有100種可能性焚挠,在1000個(gè)地方都有對(duì)這個(gè)狀態(tài)進(jìn)行操作。debug的時(shí)候要?dú)⑷说男亩加辛死焐А?shù)據(jù)不可變性同時(shí)保證了函數(shù)沒(méi)有“副作用”蝌衔,函數(shù)的副作用是指除了返回函數(shù)值外榛泛,還對(duì)主調(diào)用函數(shù)
?
函數(shù)是第一公民(first class method)函數(shù)可以像普通變量一樣去使用。函數(shù)可以像變量一樣被創(chuàng)建胚委,修改挟鸠,并當(dāng)成變量一樣傳遞,返回或是在函數(shù)中嵌套函數(shù)亩冬。
?
引用透明(referential transparency)指的是函數(shù)的運(yùn)行不依賴(lài)于外部變量或“狀態(tài)”艘希,只依賴(lài)于輸入的參數(shù),任何時(shí)候只要參數(shù)相同硅急,調(diào)用函數(shù)所得到的返回值總是相同的覆享。天然適應(yīng)并發(fā)編程,因?yàn)檎{(diào)用函數(shù)的結(jié)果具有一致性营袜,所以根本不需要加鎖撒顿,也就不存在死鎖的問(wèn)題。
?
?尾遞歸化(tail call optimization)因?yàn)楹瘮?shù)調(diào)用要壓棧保存現(xiàn)場(chǎng)荚板,遞歸層次過(guò)深的話凤壁,壓棧過(guò)多會(huì)產(chǎn)生性能問(wèn)題。所以引入尾遞歸優(yōu)化跪另,每次遞歸時(shí)都會(huì)重用棧拧抖,提升性能。
? ? ?把函數(shù)作為參數(shù)傳遞的例子
NSComparisonResult (^cmp)(idobj1,idobj2)? = ^NSComparisonResult(idobj1,id obj2) {
? ? return [obj1 isEqualToString:obj2];
}
NSArray* items = [@"a",@"c",@"d"];
[items sortedArrayUsingComparator:cmp];
sortedArrayUsingComparator方法負(fù)責(zé)排序免绿,需要把比較的規(guī)則告訴他唧席,將比較方法作為一個(gè)參數(shù)傳到函數(shù)中進(jìn)行運(yùn)算。
?3嘲驾、函數(shù)式編程技術(shù)(方法論)
?映射化簡(jiǎn)(map & reduce)函數(shù)式編程最常見(jiàn)的技術(shù)就是對(duì)一個(gè)集合做Map和Reduce操作淌哟。這比起過(guò)程式的語(yǔ)言來(lái)說(shuō),在代碼上要更容易閱讀辽故。傳統(tǒng)過(guò)程式的語(yǔ)言需要使用for/while循環(huán)徒仓,然后在各種變量中把數(shù)據(jù)倒過(guò)來(lái)倒過(guò)去的
?
管道??? (pipeline)把一組函數(shù)放到一個(gè)數(shù)組或是列表中,然后把數(shù)據(jù)傳給這個(gè)列表誊垢,數(shù)據(jù)就像一個(gè)pipeline一樣順序地被各個(gè)函數(shù)所操作蓬衡,最終得到我們想要的結(jié)果。他的設(shè)計(jì)哲學(xué)就是讓每個(gè)功能就做一件事彤枢,并把這件事做到極致狰晚,軟件或程序的拼裝會(huì)變得更為簡(jiǎn)單和直觀。
?遞歸??? (recursing)遞歸最大的好處就簡(jiǎn)化代碼缴啡,他可以把一個(gè)復(fù)雜的問(wèn)題用很簡(jiǎn)單的代碼描述出來(lái)壁晒。遞歸的精髓是描述問(wèn)題,而這正是函數(shù)式編程的精髓业栅。
?柯里化? (currying)把一個(gè)函數(shù)的多個(gè)參數(shù)分解成多個(gè)函數(shù)秒咐, 然后把函數(shù)多層封裝起來(lái)谬晕,每層函數(shù)都返回一個(gè)函數(shù)去接收下一個(gè)參數(shù)。
?高階函數(shù)(higher order function)把函數(shù)當(dāng)參數(shù)携取,接受一個(gè)函數(shù)作為參數(shù)的函數(shù)就叫高階函數(shù)≡芮現(xiàn)象上就是函數(shù)傳進(jìn)傳出。
?map & reduce
Python代碼:
def toUpper(item)
? ? return item.upper()print map(tuUpper , [“hellow”,”world”])
將數(shù)組里的所有字符串變?yōu)榇髮?xiě)雷滋,直接使用map不撑,不需要寫(xiě)for循環(huán)。最后輸出 ["HELLO","WORLD"]?
printreduce(lambdax , y : x + y,[1,2,3,4,5])
將數(shù)組里所有數(shù)值進(jìn)行累加晤斩,相當(dāng)于1+2+3+4+5焕檬,輸出 15。
lambda是Python的匿名函數(shù)澳泵,lambda x,y:x+y相當(dāng)于def func(x,y):return x+y
管道
如果有一個(gè)需求找出一組數(shù)中的所有偶數(shù)并對(duì)他們求平方实愚,最后求他們的和,可以分解成三個(gè)步驟:
1)找偶數(shù)
2)求平方
3)累加
def even(nums):
? ? ?returnfilter(lambdax: x%2==0, nums)def square(nums):
? ? returnmap(lambdax: x*x, nums)def? total(nums):
? ? returnreduce(lambdax,y:x+y,nums)
nums = [1,2,3,4,5,6,7,8,9,10]
pipeline = total(square(even(nums)))
even方法求偶數(shù)兔辅,square求他們的平方腊敲,total方法將他們加在一起。通過(guò)管道的方式把他們串聯(lián)在一起维苔,一個(gè)復(fù)雜的處理就完成了碰辅。
讓每個(gè)方法只做一件事,并把這件事做到極致蕉鸳。
?遞歸
在其他類(lèi)型的語(yǔ)言中,變量往往用來(lái)保存狀態(tài)忍法。變量不可變潮尝,意味著狀態(tài)不能保存在變量中。函數(shù)式編程使用參數(shù)保存狀態(tài)饿序,最好的例子就是遞歸勉失。
voidfun(constint i)
{
? ? if(i <10&& i >=0)
? ? {
? ? ? ? NSLog(@"i:%d", i);
? ? ? ? fun(i +1);
? ? }
}
每次狀態(tài)的變化就是值+1。
?柯里化
柯里化就是一個(gè)函數(shù)只有一個(gè)參數(shù)原探,那如果需要兩個(gè)參數(shù)怎么辦乱凿,比如兩個(gè)數(shù)相加求和。通過(guò)把一個(gè)參數(shù)封裝成函數(shù)的方式實(shí)現(xiàn)咽弦。
def func(a):
? ? def add(b):
? ? ? ? returna+b
? ? return add
funcA = func(5)printfuncA(10)
func函數(shù)返回一個(gè)add函數(shù)徒蟆,funcA變量就是一個(gè)a為5的add函數(shù)。print funcA(10)就是向add函數(shù)傳入10型型,最后結(jié)果就是5+10 輸出15段审。
?4、函數(shù)式編程意義
1闹蒜、代碼簡(jiǎn)潔寺枉,快速開(kāi)發(fā)
?函數(shù)式編程大量使用函數(shù)抑淫,減少了代碼的重復(fù),因此程序比較短姥闪,開(kāi)發(fā)速度較快始苇。
2、接近自然語(yǔ)言筐喳,易于理解
函數(shù)式編程注重干什么而不是怎么干催式,更容易理解。
3疏唾、方便代碼的管理和維護(hù)
?函數(shù)式編程不依賴(lài)蓄氧、也不會(huì)改變外界的狀態(tài),只要給定輸入?yún)?shù)槐脏,返回的結(jié)果必定相同喉童。因此,每一個(gè)函數(shù)都可以被看做獨(dú)立單元顿天,很有利于進(jìn)行單元測(cè)試 (unit testing)和除錯(cuò)(debugging)堂氯,以及模塊化組合。
4牌废、易于進(jìn)行并發(fā)開(kāi)發(fā)
函數(shù)式編程因?yàn)樗恍薷淖兞垦拾祝愿静淮嬖?鎖"線程的問(wèn)題,不需要考慮"死鎖"(deadlock)鸟缕。不必?fù)?dān)心一個(gè)線程的數(shù)據(jù)晶框,被另一個(gè)線程修改,所以可以很放心地把工作分?jǐn)偟蕉鄠€(gè)線程中懂从。
? 5授段、代碼熱升級(jí)
函數(shù)式編程沒(méi)有副作用,只要保證接口不變番甩,內(nèi)部實(shí)現(xiàn)是外部無(wú)關(guān)的侵贵。所以,可以在運(yùn)行狀態(tài)下直接升級(jí)代碼缘薛,不需要重啟窍育,也不需要停機(jī)。Erlang語(yǔ)言早就證明了這一點(diǎn)宴胧,它是瑞典愛(ài)立信公司為了管理電話系統(tǒng)而開(kāi)發(fā)的漱抓,電話系統(tǒng)的升級(jí)當(dāng)然是不能停機(jī)的。
最后恕齐,其實(shí)使用面向?qū)ο蠡蛘呙嫦蚍椒ǘ疾恢匾尚匾氖侨绾卫斫馄渲械膬r(jià)值觀和方法論,構(gòu)造可維護(hù)、可擴(kuò)展补胚、穩(wěn)定又靈活的程序码耐。不管白貓黑貓抓到老鼠就是好貓。
?參考:
http://en.wikipedia.org/wiki/Functional_programming
http://coolshell.cn/articles/10822.html
http://www.2cto.com/shouce/pythonjc/index.html
http://www.ruanyifeng.com/blog/2012/04/functional_programming.html