為什么要學(xué)習(xí)函數(shù)式編程
函數(shù)式編程是編程范式中的一種污呼,是一種典型的編程思想和方法项郊。其他的編程范式還包括面向?qū)ο缶幊?/a>偏化,邏輯編程等嗅绰。
許多人會(huì)有這樣的疑惑:為什么要學(xué)習(xí)編程范式超棺?為什么要多學(xué)習(xí)一種編程范式向族?
答案是
為了更好的模塊化
編程范式的意義在于它提供了模塊化代碼的各種思想和方法。函數(shù)式編程亦然说搅。
模塊化對(duì)工程的意義不言而喻炸枣,它是工程師的追求和驕傲。
- 模塊化使得開發(fā)更快弄唧、維護(hù)更容易
- 模塊可以重用
- 模塊化便于單元測(cè)試和debug
所謂人如其名适肠,正如面向?qū)ο缶幊淌且詫?duì)象為單位來構(gòu)建模塊一樣,如果以一句話介紹函數(shù)式編程候引,我會(huì)說:
函數(shù)式編程是以函數(shù)為核心來組織模塊的一套編程方法侯养。
文章結(jié)構(gòu)
本文首先會(huì)介紹函數(shù)式編程的兩點(diǎn)基本主張
- 函數(shù)是第一等公民
- 純函數(shù)
這兩點(diǎn)是函數(shù)式編程的基礎(chǔ),他帶來了更高層次的模塊化代碼手段澄干,是單元測(cè)試工程師的夢(mèng)想天堂逛揩。
在以上基本主張之上柠傍,函數(shù)式編程帶來了諸多酷炫的技術(shù):
- 利用Memorization提升性能
- 利用延遲求值寫出更好的模塊化代碼
- 使用currying技術(shù)進(jìn)行函數(shù)封裝
好,旅途正式開啟辩稽。
函數(shù)是第一等公民
既然函數(shù)式編程的基本理念是以函數(shù)為核心來組織代碼惧笛,很自然的,它首先將函數(shù)的地位提高逞泄,視其為“第一等公民” (first class)患整。
所謂“第一等公民”,是指函數(shù)和其他數(shù)據(jù)類型擁有平等的地位喷众,可以賦值給變量各谚,也可以作為參數(shù)傳入另一個(gè)函數(shù),或者作為別的函數(shù)的返回值到千。
當(dāng)編程語言將函數(shù)視作“第一等公民”昌渤,那么相當(dāng)于它支持了高階函數(shù),因?yàn)楦唠A函數(shù)就是至少滿足下列一個(gè)條件的函數(shù)
- 接受一個(gè)或多個(gè)函數(shù)作為輸入
- 輸出一個(gè)函數(shù)
為什么將函數(shù)視作“第一等公民”有利于寫出模塊化的代碼憔四?不妨來看這樣一個(gè)例子
有數(shù)組numberList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]膀息,編寫程序完成以下目標(biāo):
- 1.1 將numberList中的每個(gè)元素加1得到一個(gè)新的數(shù)組
- 1.2 將numberList中的每個(gè)元素乘2得到一個(gè)新的數(shù)組
- 1.3 將numberList中的每個(gè)元素模3得到一個(gè)新的數(shù)組
函數(shù)不是“第一等公民”情況下的代碼:
# 1.1 將numberList中的每個(gè)元素加1得到一個(gè)新的數(shù)組
newList = []
for num in numberList:
newList.append(num + 1)
# 1.2 將numberList中的每個(gè)元素乘2得到一個(gè)新的數(shù)組
newList = []
for num in numberList:
newList.append(num * 2)
# 1.3 將numberList中的每個(gè)元素模3得到一個(gè)新的數(shù)組
newList = []
for num in numberList:
newList.append(num % 3)
有沒有發(fā)現(xiàn)每段代碼除了加1,乘2加矛,模3的部分不一樣之外履婉,其他的代碼都是一樣的,
也就是說這三段代碼只有原數(shù)組到新數(shù)組的映射函數(shù)是不同的(分別是加1斟览,乘2毁腿,模3)。如果這個(gè)映射函數(shù)能夠以參數(shù)的方式傳遞苛茂,那么就可以復(fù)用上面的大部分代碼了已烤。我們將可復(fù)用的代碼抽取出來編寫成高階函數(shù)map
,如下
# 高階函數(shù)`map`
# 該函數(shù)接受一個(gè)函數(shù)和一個(gè)數(shù)組作為輸入妓羊,函數(shù)體中將這個(gè)函數(shù)作用于數(shù)組的每個(gè)元素然后作為返回值返回
def map(mappingFuction, numberList):
newList = []
for num in numberList:
newList.append(mappingFuction(num))
事實(shí)上幾乎所有函數(shù)式編程語言都提供了這樣的map函數(shù)胯究,于是完成上面的作業(yè)事實(shí)上只需3行代碼,如下:
(PS:lambda關(guān)鍵字用于定義一個(gè)匿名函數(shù)(anonymous function)躁绸,x表示輸入裕循,冒號(hào)后是函數(shù)體同時(shí)也是返回值)
# 1.1 將numberList中的每個(gè)元素加1得到一個(gè)新的數(shù)組
map(lambda x: x + 1, numberList)
# 1.2 將numberList中的每個(gè)元素乘2得到一個(gè)新的數(shù)組
map(lambda x: x * 2, numberList)
# 1.3 將numberList中的每個(gè)元素模3得到一個(gè)新的數(shù)組
map(lambda x: x % 3, numberList)
除了map函數(shù)之外,一般函數(shù)式編程語言還會(huì)配套提供一些非常通用的高階函數(shù)净刮,使得寫出的代碼就像是對(duì)原問題的一句描述剥哑,簡(jiǎn)練又易讀——有時(shí)人們也稱這樣的編程風(fēng)格為聲明式編程 (declarative_programming)。而前一種代碼看起來是一步一步的求解步驟淹父,這種編程風(fēng)格被稱為指令式編程 (imperative_programming)株婴。
純函數(shù)
除了將函數(shù)視作“一等公民”,函數(shù)式編程語言還主張甚至強(qiáng)制將函數(shù)寫成純函數(shù) (pure function)暑认。
純函數(shù)是指同時(shí)滿足下面兩個(gè)條件的函數(shù):
- 函數(shù)的結(jié)果只依賴于輸入的參數(shù)且與外部系統(tǒng)狀態(tài)無關(guān)——只要輸入相同困介,返回值總是不變的大审。
- 除了返回值外,不修改程序的外部狀態(tài)(比如全局變量座哩、入?yún)ⅲ┩椒觥!獫M足這個(gè)條件也被稱作“沒有副作用 (side effect)”
由純函數(shù)的兩點(diǎn)條件可以看出八回,純函數(shù)是相對(duì)獨(dú)立的程序構(gòu)件酷愧。因?yàn)楹瘮?shù)的結(jié)果只依賴于輸入的參數(shù)且與外部系統(tǒng)狀態(tài)無關(guān),使得單元測(cè)試和debug變得異常容易缠诅,而這也正是模塊化的優(yōu)點(diǎn)之一。
除此之外乍迄,可以并發(fā)執(zhí)行是純函數(shù)的另一優(yōu)點(diǎn)管引,比如如下代碼
t1 = pureFunction1(arg1)
t2 = pureFunction2(arg2)
result = concatFuction(t1, t2)
由于純函數(shù)pureFunction1
和pureFunction2
只與入?yún)⑾嚓P(guān)而不依賴其他的外部系統(tǒng)狀態(tài),前兩句函數(shù)調(diào)用的執(zhí)行順序與程序結(jié)果是無關(guān)的闯两,完全可以并發(fā)執(zhí)行褥伴。
當(dāng)人們討論函數(shù)式編程的時(shí)候,常常會(huì)提到一個(gè)詞——引用透明(Referential transparency)漾狼。其實(shí)引用透明的概念與純函數(shù)很接近:
如果一個(gè)表達(dá)式重慢,對(duì)于相同的輸入,總是有相同的結(jié)果并且不修改程序其他部分的狀態(tài)逊躁,那么這個(gè)表達(dá)式是引用透明的似踱。
由前面純函數(shù)的定義可以看到,由于函數(shù)調(diào)用也是表達(dá)式的一種稽煤,因此任何純函數(shù)的調(diào)用都滿足引用透明核芽。
作為一等公民的純函數(shù)還帶來了什么
函數(shù)式編程語言中一等公民的函數(shù)地位,以及純函數(shù)的強(qiáng)制要求酵熙,可以帶來諸多好處轧简。
(1)可以利用Memoization技術(shù)提升性能。
滿足引用透明的表達(dá)式(包括任意純函數(shù)調(diào)用)滿足這樣一個(gè)特點(diǎn)匾二,就是任意兩次調(diào)用只要輸入相同哮独,其結(jié)果總是不變的。于是可以將第一次的計(jì)算結(jié)果緩存起來察藐,遇到下一次執(zhí)行時(shí)直接替換皮璧,依然能保證程序的正確性。這種優(yōu)化方法稱為Memoization转培。
(2)延遲求值 ( Lazy Evaluation )
延遲求值是指表達(dá)式不在它被綁定到變量時(shí)就立即求值恶导,而是在該值被用到的時(shí)候才計(jì)算求值。
很顯然浸须,延遲求值的正確性需要純函數(shù)的性質(zhì)來保證——即在輸入?yún)?shù)相同的情況下惨寿,無論什么時(shí)候被執(zhí)行邦泄,結(jié)果總是不變的。
延遲求值有利于程序性能的提升裂垦。
比如下面這段代碼顺囊,trace函數(shù)在debugFlag等于True時(shí)會(huì)將debug信息打印到標(biāo)準(zhǔn)輸出。
def trace(debugFlag, debugStr):
if debug == True:
print debugStr
在實(shí)際使用中debug信息可能會(huì)由幾部分拼接而成蕉拢,如下:
trace(debugFlag, 'str1' + 'str2' + 'str3')
如果是沒有延遲求值的語言特碳,無論debugFlag參數(shù)等于True還是Flase, 'str1' + 'str2' + 'str3'
這段代碼都是會(huì)被執(zhí)行的晕换。
而如果是采用延遲求值策略午乓,只要當(dāng)debug參數(shù)等于True且真正執(zhí)行到print語句時(shí),字符串拼接代碼才會(huì)被執(zhí)行闸准。也就是說真正被用到時(shí)才執(zhí)行益愈。
延遲求值更大的好處仍是利于模塊化,而且是超酷的模塊化夷家。比如思考求解下面這組問題蒸其。
- 1.1 輸出斐波那契數(shù)列的第10個(gè)到第20個(gè)數(shù)
- 1.2 輸出斐波那契數(shù)列中前十個(gè)偶數(shù)
- 1.3 輸出斐波那契數(shù)列數(shù)列的前五個(gè)能被3整除的數(shù)
如果不考慮具體的語言和實(shí)現(xiàn),可以將問題拆解成幾個(gè)函數(shù)库快。一個(gè)函數(shù)負(fù)責(zé)生成斐波那契數(shù)列摸袁,一個(gè)函數(shù)負(fù)責(zé)篩選數(shù)列中的偶數(shù),再寫個(gè)函數(shù)挑出任意數(shù)列中能被3整除的數(shù)义屏。
將第一個(gè)函數(shù)的輸出作為后面兩個(gè)函數(shù)的輸入靠汁,問題就得到解決了。
但問題是斐波那契數(shù)列是一個(gè)無窮數(shù)列湿蛔,一般的語言無法輸出一個(gè)無窮的數(shù)據(jù)結(jié)構(gòu)膀曾。不過這對(duì)于支持延遲求值的語言來說不成什么問題,因?yàn)槊總€(gè)值只有在真正被用到的時(shí)候才會(huì)被計(jì)算出來阳啥,因此完全可以像這樣定義一組無窮的斐波那契數(shù)列
fiboSequence = createFibonacci()
然后完成上面三道題只需這樣
- 1.1 輸出斐波那契數(shù)列的第10個(gè)到第20個(gè)數(shù)
print fiboSequence[10 : 20] #數(shù)列中第20個(gè)之后的數(shù)不會(huì)被計(jì)算
- 1.2 輸出斐波那契數(shù)列中前十個(gè)偶數(shù)
evenFiboSequence = pickEven(fiboSequence) # 函數(shù)pickEven從斐波那契數(shù)列中挑出所有的偶數(shù)添谊,此時(shí)并不會(huì)真正計(jì)算
print evenFiboSequence[0 : 10] #直到輸出時(shí)才會(huì)把用到的值計(jì)算出來
- 1.3 輸出斐波那契數(shù)列數(shù)列的前五個(gè)能被3整除的數(shù)
newList = pick3(fiboSequence) #函數(shù)pick3從斐波那契數(shù)列中挑出所有能被3整除的數(shù),此時(shí)并不會(huì)真正計(jì)算
print newLIst[0 : 5] #直到輸出時(shí)才會(huì)把用到的值計(jì)算出來
(3)可以使用 currying 技術(shù)進(jìn)行函數(shù)封裝
curryiny 將接受多個(gè)參數(shù)的函數(shù)變換成接受其中部分參數(shù)察迟,并且返回接受余下參數(shù)的新函數(shù)斩狱。(維基百科中的定義是接受一個(gè)單一參數(shù)的新函數(shù),然而現(xiàn)實(shí)中currying技術(shù)的涵義被延伸了扎瓶。)
比如冪函數(shù)pow(x, y)
所踊,它接受兩個(gè)參數(shù)——x和y,計(jì)算x^y概荷。使用currying技術(shù)秕岛,可以將y固定為2轉(zhuǎn)化為只接受單一參數(shù)x的平方函數(shù),或者將y固定為3轉(zhuǎn)化為立方函數(shù)。代碼如下:
#平方函數(shù)
def square (int x):
return pow(x, 2)
#立方函數(shù)
def cube (int x):
return pow(x, 3)
熟悉設(shè)計(jì)模式的朋友已經(jīng)感覺到继薛,currying完成的事情就是函數(shù)(接口)封裝修壕,它將一個(gè)已有的函數(shù)(接口)做封裝,得到一個(gè)新的函數(shù)(接口)遏考,這與適配器模式(Adapter pattern)的思想是一致的慈鸠。但由于函數(shù)式編程語言更高的抽象層次,使得許多使用者甚至感覺不到自己在使用函數(shù)封裝灌具,它的語法太直觀自然了青团。比如使用python語言functools
中提供的partial
函數(shù),currying只需一行代碼咖楣,如下
#將pow函數(shù)的x參數(shù)固定為2督笆,返回平方函數(shù)
square = partial.partial(pow, x=2)
#將pow函數(shù)的x參數(shù)固定為3,返回立方函數(shù)
cube = partial.partial(pow, x=3)
總結(jié)
最后用兩句話來歸納全文:
函數(shù)式編程通過
1)支持高階函數(shù)
2)倡導(dǎo)純函數(shù)
的設(shè)計(jì)截歉,使得它在模塊化設(shè)計(jì)胖腾、單元測(cè)試、并行化方面具有獨(dú)特的魅力瘪松。
在這樣的設(shè)計(jì)之下
1)利用Memorization提升性能
2)利用延遲求值模塊化代碼
3)使用currying技術(shù)進(jìn)行函數(shù)封裝
這些炫酷的技術(shù)成為現(xiàn)實(shí)。
我愛函數(shù)式編程锨阿。
參考資料
Why Functional Programming Matters —— John Hughes
函數(shù)式編程 —— 陳浩
函數(shù)式編程初探 —— 阮一峰
Functional Programming For The Rest of Us —— Slava Akhmechet
傻瓜函數(shù)式編程