前言
我不知道你都收藏了些什么们颜,我的閱讀清單里面相當(dāng)大部分都是函數(shù)式編程相關(guān)的東東:基本上是最難啃的。這些文章充斥著無比枯燥的教科書語言斯棒,我想就連那些在華爾街浸淫10年以上的大牛都無法搞懂這些函數(shù)式編程(簡稱FP)。你可以去花旗集團或者德意志銀行找個項目經(jīng)理來問問1:你們?yōu)槭裁匆xJMS而不用Erlang是越?答案基本上是:我認(rèn)為這個學(xué)術(shù)用的語言還無法勝任實際應(yīng)用。可是,現(xiàn)有的一些系統(tǒng)不僅非常復(fù)雜還需要滿足十分嚴(yán)苛的需求薪贫,它們就都是用函數(shù)式編程的方法來實現(xiàn)的。這刻恭,就說不過去了瞧省。
關(guān)于FP的文章確實比較難懂,但我不認(rèn)為一定要搞得那么晦澀。有一些歷史原因造成了這種知識斷層鞍匾,可是FP概念本身并不難理解交洗。我希望這篇文章可以成為一個“FP入門指南”,幫助你從指令式編程走向函數(shù)式編程橡淑。先來點咖啡构拳,然后繼續(xù)讀下去。很快你對FP的理解就會讓同事們刮目相看了梳码。
什么是函數(shù)式編程(Functional Programming隐圾,F(xiàn)P)伍掀?它從何而來掰茶?可以吃嗎?倘若它真的像那些鼓吹FP的人說的那么好蜜笤,為什么實際應(yīng)用中那么少見濒蒋?為什么只有那些在讀博士的家伙想要用它?而最重要的是把兔,它母親的怎么就那么難學(xué)沪伙?那些所謂的closure、continuation县好,currying围橡,lazy evaluation還有no side effects都是什么東東(譯者:本著保留專用術(shù)語的原則,此處及下文類似情形均不譯)缕贡?如果沒有那些大學(xué)教授的幫忙怎樣把它應(yīng)用到實際工程里去翁授?為什么它和我們熟悉的萬能而神圣的指令式編程那么的不一樣?
函數(shù)式概念
又稱泛函編程晾咪,是一種編程范型收擦,它將電腦運算視為數(shù)學(xué)上的函數(shù)計算,并且避免使用程序狀態(tài)以及易變對象谍倦。函數(shù)編程語言最重要的基礎(chǔ)是λ演算(lambda calculus)塞赂。而且λ演算的函數(shù)可以接受函數(shù)當(dāng)作輸入(引數(shù))和輸出(傳出值)。
比起命令式編程昼蛀,函數(shù)式編程更加強調(diào)程序執(zhí)行的結(jié)果而非執(zhí)行的過程宴猾,倡導(dǎo)利用若干簡單的執(zhí)行單元讓計算結(jié)果不斷漸進,逐層推導(dǎo)復(fù)雜的運算叼旋,而不是設(shè)計一個復(fù)雜的執(zhí)行過程鳍置。結(jié)果比過程更重要送淆。
函數(shù)式編程中的函數(shù)這個術(shù)語不是指計算機中的函數(shù)(實際上是Subroutine)税产,而是指數(shù)學(xué)中的函數(shù),即自變量的映射。也就是說一個函數(shù)的值僅決定于函數(shù)參數(shù)的值辟拷,不依賴其他狀態(tài)撞羽。比如sqrt(x)函數(shù)計算x的平方根,只要x不變衫冻,不論什么時候調(diào)用诀紊,調(diào)用幾次,值都是不變的隅俘。函數(shù)式編程為我們提供了另外一種抽象和思考的方式邻奠。函數(shù)式編程是一種風(fēng)格與編程語言無關(guān), 面向?qū)ο笠彩且环N風(fēng)格與編程語言無關(guān)为居,兩種風(fēng)格并不矛盾碌宴,可以結(jié)合的- 叫 functional object(Objects in OCaml)
函數(shù)式特性
-
閉包
又稱詞法閉包(Lexical Closure)、函數(shù)閉包(function closures)或者lambdas表達式蒙畴,是引用了自由變量的函數(shù)贰镣。這個被引用的自由變量將和這個函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外膳凝。所以碑隆,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。
對應(yīng)于OC語言中的Blocks蹬音。
閉包可以理解為匿名函數(shù)上煤,后面的示例將大量使用閉包,閱讀代碼時著淆,請把Block當(dāng)作函數(shù)。
因為閉包只有在被調(diào)用時才執(zhí)行操作嘉熊,即“惰性求值”孕惜,所以它可以被用來定義控制結(jié)構(gòu)瓮栗。
?
-
惰性求值
執(zhí)行順序不依賴語句順序进陡,更易并發(fā)糙麦。
函數(shù)式編程語言還提供惰性求值(Lazy evaluation仆邓,也稱作call-by-need)榜聂,是在將表達式賦值給變量(或稱作綁定)時并不計算表達式的值豌汇,而在變量第一次被使用時才進行計算逻澳。這樣就可以通過避免不必要的求值提升性能瓤逼。
具體理解,參考閉包。
?
-
函數(shù)是"第一等公民"
所謂"第一等公民"(first class),指的是函數(shù)與其他數(shù)據(jù)類型一樣沟绪,處于平等地位辈毯,可以賦值給其他變量坝疼,也可以作為參數(shù)钝凶,傳入另一個函數(shù)锌介,或者作為別的函數(shù)的返回值。
比較容易理解,不舉例說明。
?
-
高階函數(shù)
高階函數(shù)就是參數(shù)為函數(shù)或返回值為函數(shù)的函數(shù)∪辏現(xiàn)象上就是函數(shù)傳進傳出慢哈,就像面向?qū)ο髮ο鬂M天飛一樣兰绣。有了高階函數(shù),就可以將復(fù)用的粒度降低到函數(shù)級別方妖,相對于面向?qū)ο笳Z言狭魂,復(fù)用的粒度更低党觅。
這時候我們關(guān)注函數(shù)與函數(shù)之間的關(guān)系。
舉例來說斋泄,假設(shè)有如下的三個函數(shù)魁莉,
// 例子1: typedef NSInteger(^BlockFun)(NSInteger); BlockFun retSelf = ^(NSInteger a){ return a;}; BlockFun square = ^(NSInteger a){ return a*a;}; BlockFun cube = ^(NSInteger a){ return a*a*a;}; NSInteger sumInt(NSInteger a, NSInteger b){ if (a > b) return 0; return (a + sumInt(a + 1, b)); } NSInteger sumSquare(NSInteger a, NSInteger b){ if (a > b) return 0; return (square(a) + sumSquare(a + 1, b)); } NSInteger sumCube(NSInteger a, NSInteger b){ if (a > b) return 0; return (cube(a) + sumCube(a + 1, b)); }
分別是求a到b之間整數(shù)之和,求a到b之間整數(shù)的平方和募胃,求a到b之間整數(shù)的立方和旗唁。
三個函數(shù)不同的只是其中的fun不同,那么是否可以抽象出一個共同的模式呢痹束?
我們可以定義一個高階函數(shù)sumWithFun:
NSInteger sumWithFun(BlockFun fun , NSInteger a, NSInteger b){ if (a > b) return 0; return (fun(a) + sumWithFun(fun, a + 1, b)); }
其中參數(shù)fun是一個函數(shù)检疫,在函數(shù)中調(diào)用fun函數(shù)進行計算,并進行求和祷嘶。
然后例子1就可以簡化如下:
// 例子1: typedef NSInteger(^BlockFun)(NSInteger); BlockFun retSelf = ^(NSInteger a){ return a;}; BlockFun square = ^(NSInteger a){ return a*a;}; BlockFun cube = ^(NSInteger a){ return a*a*a;}; NSInteger sumWithFun(BlockFun fun , NSInteger a, NSInteger b){ if (a > b) return 0; return (fun(a) + sumWithFun(fun, a + 1, b)); } // 調(diào)用方式 NSInteger a = 10, b = 20; NSLog(@"%ld, %ld, %ld", sumWithFun(retSelf, a, b), sumWithFun(square, a, b), sumWithFun(cube, a, b) );
這樣就可以重用sumWithFun函數(shù)來實現(xiàn)三個函數(shù)中的求和邏輯屎媳。
(示例來源:https://d396qusza40orc.cloudfront.net/progfun/lecture_slides/week2-2.pdf)高階函數(shù)提供了一種函數(shù)級別上的依賴注入(或反轉(zhuǎn)控制)機制夺溢,在上面的例子里,sumWithFun函數(shù)的邏輯依賴于注入進來的函數(shù)的邏輯烛谊。很多GoF設(shè)計模式都可以用高階函數(shù)來實現(xiàn)风响,如Visitor,Strategy丹禀,Decorator等状勤。比如Visitor模式就可以用集合類的map()或foreach()高階函數(shù)來替代。
?
-
Continuation
我們對函數(shù)的理解只有一半是正確的双泪,因為這樣的理解基于一個錯誤的假設(shè):函數(shù)一定要把其返回值返回給調(diào)用者荧降。按照這樣的理解,continuation就是更加廣義的函數(shù)攒读。這里的函數(shù)不一定要把返回值傳回給調(diào)用者朵诫,相反,它可以把返回值傳給程序中的任意代碼薄扁。continuation就是一種特別的參數(shù)剪返,把這種參數(shù)傳到函數(shù)中,函數(shù)就能夠根據(jù)continuation將返回值傳遞到程序中的某段代碼中邓梅。說得很高深脱盲,實際上沒那么復(fù)雜。直接來看看下面的例子好了:
int i = add(5, 10); int j = square(i);
add這個函數(shù)將返回15然后這個值會賦給i日缨,這也是add被調(diào)用的地方钱反。接下來i的值又會被用于調(diào)用square。請注意支持惰性求值的編譯器是不能打亂這段代碼執(zhí)行順序的匣距,因為第二個函數(shù)的執(zhí)行依賴于第一個函數(shù)成功執(zhí)行并返回結(jié)果面哥。這段代碼可以用Continuation Pass Style(CPS)技術(shù)重寫,這樣一來add的返回值就不是傳給其調(diào)用者毅待,而是直接傳到square里去了尚卫。
int j = add(5, 10, square);
在上例中,add多了一個參數(shù):一個函數(shù)尸红,add必須在完成自己的計算后吱涉,調(diào)用這個函數(shù)并把結(jié)果傳給它。這時square就是add的一個continuation外里。上面兩段程序中j的值都是225怎爵。
這樣,我們學(xué)習(xí)到了強制惰性語言順序執(zhí)行兩個表達式的第一個技巧盅蝗。再來看看下面IO程序(是不是有點眼熟鳖链?):
System.out.println("Please enter your name: "); System.in.readLine();
這兩行代碼彼此之間沒有依賴關(guān)系,因此編譯器可以隨意的重新安排它們的執(zhí)行順序风科∪雎郑可是只要用CPS重寫它乞旦,編譯器就必須順序執(zhí)行了,因為重寫后的代碼存在依賴關(guān)系了题山。
System.out.println("Please enter your name: ", System.in.readLine);
-
柯里化(局部調(diào)用(partial application))
維基百科定義:
是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù)兰粉,并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
當(dāng)一個函數(shù)沒有傳入全部所需參數(shù)時顶瞳,它會返回另一個函數(shù)(這個返回的函數(shù)會記錄那些已經(jīng)傳入的參數(shù))玖姑,這種情況叫作柯里化。
在直覺上慨菱,柯里化聲稱“如果你固定某些參數(shù)焰络,你將得到接受余下參數(shù)的一個函數(shù)”。
比如冪函數(shù)pow(x, y)
符喝,它接受兩個參數(shù)——x和y闪彼,計算x^y。使用柯里化技術(shù)协饲,可以將y固定為2轉(zhuǎn)化為只接受單一參數(shù)x的平方函數(shù)畏腕,或者將y固定為3轉(zhuǎn)化為立方函數(shù)。代碼如下:// 平方函數(shù) double(^SquareFun)(double) = ^(double a){ return pow(a, 2); }; // 立方函數(shù) double(^CubeFun)(double) = ^(double a){ return pow(a, 3); };
熟悉設(shè)計模式的朋友已經(jīng)感覺到茉稠,currying完成的事情就是函數(shù)(接口)封裝描馅,它將一個已有的函數(shù)(接口)做封裝,得到一個新的函數(shù)(接口)而线,這與適配器模式(Adapter pattern)的思想是一致的铭污。
?
為什么要柯里化:
延遲計算。上面的例子已經(jīng)比較好地說明了膀篮。
參數(shù)復(fù)用嘹狞。當(dāng)在多次調(diào)用同一個函數(shù),并且傳遞的參數(shù)絕大多數(shù)是相同的各拷,那么該函數(shù)可能是一個很好的柯里化候選刁绒。
動態(tài)創(chuàng)建函數(shù)。這可以是在部分計算出結(jié)果后烤黍,在此基礎(chǔ)上動態(tài)生成新的函數(shù)處理后面的業(yè)務(wù),這樣省略了重復(fù)計算傻盟∷偃铮或者可以通過將要傳入調(diào)用函數(shù)的參數(shù)子集,部分應(yīng)用到函數(shù)中娘赴,從而動態(tài)創(chuàng)造出一個新函數(shù)规哲,這個新函數(shù)保存了重復(fù)傳入的參數(shù)(以后不必每次都傳)。
思考:高階函數(shù)中舉的例子sumWithFun如何柯里化诽表?
-
只用"表達式"唉锌,不用"語句"
"表達式"(expression)是一個單純的運算過程隅肥,總是有返回值;"語句"(statement)是執(zhí)行某種操作袄简,沒有返回值腥放。函數(shù)式編程要求,只使用表達式绿语,不使用語句秃症。也就是說,每一步都是單純的運算吕粹,而且都有返回值种柑。
原因是函數(shù)式編程的開發(fā)動機,一開始就是為了處理運算(computation)匹耕,不考慮系統(tǒng)的讀寫(I/O)聚请。"語句"屬于對系統(tǒng)的讀寫操作,所以就被排斥在外稳其。
當(dāng)然良漱,實際應(yīng)用中,不做I/O是不可能的欢际。因此母市,編程過程中,函數(shù)式編程只要求把I/O限制到最小损趋,不要有不必要的讀寫行為患久,保持計算過程的單純性。
一切皆表達式思維:if b then 100 else 10浑槽,這不是條件跳轉(zhuǎn)蒋失,而是一個三元表達式,返回100或者10桐玻。
?
-
不修改狀態(tài)
上一點已經(jīng)提到篙挽,函數(shù)式編程只是返回新的值,不修改系統(tǒng)變量镊靴。因此铣卡,不修改變量,也是它的一個重要特點偏竟。
在其他類型的語言中煮落,變量往往用來保存"狀態(tài)"(state)。不修改變量踊谋,意味著狀態(tài)不能保存在變量中蝉仇。函數(shù)式編程使用參數(shù)保存狀態(tài),最好的例子就是遞歸。下面的代碼是一個將字符串逆序排列的函數(shù)轿衔,它演示了不同的參數(shù)如何決定了運算所處的"狀態(tài)"沉迹。let a = 100,意義不是把100賦值給變量a害驹,而是把a符號綁定(或者叫匹配)到100鞭呕。
由于變量值是不可變的,對于值的操作并不是修改原來的值裙秋,而是修改新產(chǎn)生的值琅拌,原來的值保持不便。例如一個Point類摘刑,其moveBy方法不是改變已有Point實例的x和y坐標(biāo)值进宝,而是返回一個新的Point實例。
class Point(x: Int, y: Int){ override def toString() = "Point (" + x + ", " + y + ")" def moveBy(deltaX: Int, deltaY: Int) = { new Point(x + deltaX, y + deltaY) } }
(示例來源:Anders Hejlsberg在echDays 2010上的演講)
同樣由于變量不可變枷恕,純函數(shù)編程語言無法實現(xiàn)循環(huán)党晋,這是因為For循環(huán)使用可變的狀態(tài)作為計數(shù)器,而While循環(huán)或DoWhile循環(huán)需要可變的狀態(tài)作為跳出循環(huán)的條件徐块。因此在函數(shù)式語言里就只能使用遞歸來解決迭代問題未玻,這使得函數(shù)式編程嚴(yán)重依賴遞歸。
?
通常來說胡控,算法都有遞推(iterative)和遞歸(recursive)兩種定義扳剿,以階乘為例,階乘的遞推定義為:
而階乘的遞歸定義
遞推定義的計算時需要使用一個累積器保存每個迭代的中間計算結(jié)果昼激,C代碼如下:static int fact(int n){ int acc = 1; for(int k = 1; k <= n; k++){ acc = acc * k; } return acc; }
而遞歸定義的計算的C代碼如下:
int fact(int n){ if(n == 0) return 1 return n * fact(n-1) }
我們可以看到庇绽,沒有使用循環(huán),沒有使用可變的狀態(tài)橙困,函數(shù)更短小瞧掺,不需要顯示地使用累積器保存中間計算結(jié)果,而是使用參數(shù)n(在棧上分配)來保存中間計算結(jié)果凡傅。
(示例來源:1. Recursion)?
-
pipeline
這個技術(shù)的意思是辟狈,把函數(shù)實例成一個一個的action,然后夏跷,把一組action放到一個數(shù)組或是列表中哼转,然后把數(shù)據(jù)傳給這個action list,數(shù)據(jù)就像一個pipeline一樣順序地被各個函數(shù)所操作拓春,最終得到我們想要的結(jié)果释簿,如下的StringFunCompose函數(shù)。
typedef NSString*(^StringBlock)(NSString*); StringBlock ToUpperCase = ^(NSString*str){ return [str uppercaseString]; }; StringBlock Excailm = ^(NSString*str){ return [str stringByAppendingString:@"!"]; }; //// 可讀性不好硼莽,讓代碼從右向左運行,而不是由內(nèi)而外運行 StringBlock Shout = ^(NSString* str){ return Excailm(ToUpperCase(str)); }; StringBlock StringFunCompose(NSArray*blocks) { return ^(NSString* str){ NSEnumerator *enumerator = [blocks reverseObjectEnumerator]; StringBlock block; while (block = [enumerator nextObject]) { str = block(str); } return str; }; } void sampleCurry(){ NSLog(@"[sampleCurry]"); NSLog(@"%@", Shout(@"Let's get started with FP")); NSLog(@"%@", StringFunCompose(@[Excailm, ToUpperCase])(@"Let's get started with FP")); }
?
?
函數(shù)式與面向?qū)ο蟆⒚嫦蜻^程區(qū)別
面向?qū)ο缶幊桃彩且环N命令式編程懂鸵。
命令式編程是面向計算機硬件的抽象偏螺,有變量(對應(yīng)著存儲單元),賦值語句(獲取匆光,存儲指令)套像,表達式(內(nèi)存引用和算術(shù)運算)和控制語句(跳轉(zhuǎn)指令),一句話终息,命令式程序就是一個馮諾依曼機的指令序列夺巩。面向?qū)ο笾皇墙7绞讲煌瑢嵸|(zhì)也是一種命令式編程周崭。
維基百科上說柳譬,函數(shù)式編程是聲明式編程的一種。而聲明式編程則是與命令式編程相對的反義詞续镇。
-
抽象思維方式
函數(shù)式編程關(guān)心數(shù)據(jù)的映射美澳,命令式編程關(guān)心解決問題的步驟
函數(shù)式考慮數(shù)據(jù)的變換過程,A->B->C->D->E摸航;而面向?qū)ο笾聘紤],我應(yīng)該有哪些對象酱虎,每個對象實現(xiàn)哪些過程函數(shù)雨膨,過程之間如何協(xié)作。
-
變量無狀態(tài)读串,沒有賦值
純函數(shù)式編程語言中的變量也不是命令式編程語言中的變量聊记,即存儲狀態(tài)的單元,而是代數(shù)中的變量爹土,即一個值的名稱甥雕。變量的值是不可變的(immutable),也就是說不允許像命令式編程語言中那樣多次給一個變量賦值胀茵。比如說在命令式編程語言我們寫“x = x + 1”社露,這依賴可變狀態(tài)的事實,拿給程序員看說是對的琼娘,但拿給數(shù)學(xué)家看峭弟,卻被認(rèn)為這個等式為假。
?
函數(shù)式的優(yōu)點
-
沒有"副作用"
所謂"副作用"(side effect)脱拼,指的是函數(shù)內(nèi)部與外部互動(最典型的情況瞒瘸,就是修改全局變量的值),產(chǎn)生運算以外的其他結(jié)果熄浓。
函數(shù)式編程強調(diào)沒有"副作用"情臭,意味著函數(shù)要保持獨立,所有功能就是返回一個新的值,沒有其他行為俯在,尤其是不得修改外部變量的值竟秫。?
-
函數(shù)引用透明,是純函數(shù)
引用透明(Referential transparency)跷乐,指的是函數(shù)的運行不依賴于外部變量或"狀態(tài)"肥败,只依賴于輸入的參數(shù),任何時候只要參數(shù)相同愕提,引用函數(shù)所得到的返回值總是相同的馒稍。
有了不修改變量特性,這點是很顯然的浅侨。其他類型的語言纽谒,函數(shù)的返回值往往與系統(tǒng)狀態(tài)有關(guān),不同的狀態(tài)之下仗颈,返回值是不一樣的佛舱。這就叫"引用不透明",很不利于觀察和理解程序的行為挨决。
?
-
可移植性/自文檔化(Portable / Self-Documenting)
由于不依賴于函數(shù)外的變量请祖,因此可移植性好;純函數(shù)是完全自給自足的脖祈,它需要的所有東西都能輕易獲得肆捕。仔細(xì)思考思考這一點...這種自給自足的好處是什么呢?首先盖高,純函數(shù)的依賴很明確慎陵,因此更易于觀察和理解——沒有偷偷摸摸的小動作。
?
-
代碼簡潔喻奥,開發(fā)快速
函數(shù)式編程大量使用函數(shù)席纽,減少了代碼的重復(fù),因此程序比較短撞蚕,開發(fā)速度較快润梯。
?
-
復(fù)用粒度最小
函數(shù)式的利用粒度更小,以函數(shù)為單位甥厦。
?
-
接近自然語言纺铭,易于理解
函數(shù)式編程的自由度很高,可以寫出很接近自然語言的代碼刀疙。
前文曾經(jīng)將表達式(1 + 2) * 3 - 4舶赔,寫成函數(shù)式語言:
subtract(multiply(add(1,2), 3), 4)
對它進行變形,不難得到另一種寫法:
add(1,2).multiply(3).subtract(4)
? 這基本就是自然語言的表達了谦秧。再看下面的代碼竟纳,大家應(yīng)該一眼就能明白它的意思吧:
merge([1,2],[3,4]).sort().search("2")
?
-
易于測試撵溃,不容易出錯
函數(shù)即不依賴外部的狀態(tài)也不修改外部的狀態(tài),函數(shù)調(diào)用的結(jié)果不依賴調(diào)用的時間和位置蚁袭,這樣寫的代碼容易進行推理征懈,不容易出錯石咬。這使得單元測試和調(diào)試都更容易揩悄。
程序中的狀態(tài)不好維護,在并發(fā)的時候更不好維護鬼悠。(你可以試想一下如果你的程序有個復(fù)雜的狀態(tài)删性,當(dāng)以后別人改你代碼的時候,是很容易出bug的焕窝,在并行中這樣的問題就更多了)
?
-
易于"并發(fā)編程"
函數(shù)式編程不需要考慮"死鎖"(deadlock)蹬挺,因為它不修改變量,所以根本不存在"鎖"線程的問題它掂。不必?fù)?dān)心一個線程的數(shù)據(jù)巴帮,被另一個線程修改,所以可以很放心地把工作分?jǐn)偟蕉鄠€線程虐秋,部署"并發(fā)編程"(concurrency)榕茧。
請看下面的代碼:
var s1 = Op1(); var s2 = Op2(); var s3 = concat(s1, s2);
由于s1和s2互不干擾,不會修改變量客给,誰先執(zhí)行是無所謂的用押,所以可以放心地增加線程,把它們分配在兩個線程上完成靶剑。其他類型的語言就做不到這一點蜻拨,因為s1可能會修改系統(tǒng)狀態(tài),而s2可能會用到這些狀態(tài)桩引,所以必須保證s2在s1之后運行缎讼,自然也就不能部署到其他線程上了。
多核CPU是將來的潮流坑匠,所以函數(shù)式編程的這個特性非常重要血崭。
?
-
代碼的熱升級
函數(shù)式編程沒有副作用,只要保證接口不變笛辟,內(nèi)部實現(xiàn)是外部無關(guān)的届榄。所以,可以在運行狀態(tài)下直接升級代碼绿饵,不需要重啟娃闲,也不需要停機。Erlang語言早就證明了這一點围来,它是瑞典愛立信公司為了管理電話系統(tǒng)而開發(fā)的跺涤,電話系統(tǒng)的升級當(dāng)然是不能停機的匈睁。
?
函數(shù)式的缺點
-
不擅長處理可變狀態(tài)和IO
處理可變狀態(tài)和處理IO,要么引入可變變量桶错,要么通過Monad來進行封裝(如State Monad和IO Monad)航唆。
System.out.println("Please enter your name: "); System.in.readLine();
在惰性語言中沒人能保證第一行會中第二行之前執(zhí)行!這也就意味著我們不能處理IO院刁,不能調(diào)用系統(tǒng)函數(shù)做任何有用的事情(這些函數(shù)需要按照順序執(zhí)行糯钙,因為它們依賴于外部狀態(tài)),也就是說不能和外界交互了退腥!如果在代碼中引入支持順序執(zhí)行的代碼原語任岸,那么我們就失去了用數(shù)學(xué)方式分析處理代碼的優(yōu)勢(而這也意味著失去了函數(shù)式編程的所有優(yōu)勢)。
?
從函數(shù)式角度看面向?qū)ο?/h3>
? 看看維基百科中聲明式編程的定義:
聲明式編程是告訴計算機需要計算“什么”而不是“如何”去計算
任何沒有副作用的編程語言狡刘,或者更確切一點享潜,任何引用透明的編程語言
-
任何有嚴(yán)格計算邏輯的編程語言
?
我個人理解,這三個特點中的前兩個都是為了模仿人類的思維方式嗅蔬。
?
因為人類思考主要靠直覺剑按,人類自己也搞不清楚自己怎么想出答案的。所以聲明式編程語言也不要規(guī)定電腦具體怎么執(zhí)行命令澜术。
因為人類很固執(zhí)艺蝴,腦海中已有的概念很難修改。所以聲明式編程也要模仿人類瘪板,不允許修改變量吴趴。
第三點則是人工智能先驅(qū)的美好愿望,像人類一樣思考侮攀,但是別像人類一樣犯錯锣枝。
-
抽象方式
-
面向?qū)ο笾g需要協(xié)作,導(dǎo)致對象之間關(guān)系錯綜復(fù)雜兰英,不好理清撇叁。
建議:
-[x] 可以將功能抽象成對象,但抽象的基礎(chǔ)是數(shù)據(jù)對象的映射畦贸,每個對象只實現(xiàn)自己的功能陨闹,和其它對象沒有任何耦合。
-[x] 流程圖上的一個節(jié)點抽象成一個對象薄坏,最終有一個對象或者函數(shù)負(fù)責(zé)將所有的對象連接在一起趋厉,完成整個流程。這時候胶坠,可以從這個對象或者函數(shù)可以清晰地看到整個流程
-
三大特性
-
封裝
- 可讀性差君账、調(diào)試?yán)щy
面向?qū)ο笏枷雽⒆兞糠庋b到類里,然后使用函數(shù)對其進行操作沈善,成員變量的作用域在整個類乡数,需要考慮變量隨著時間的變化椭蹄。導(dǎo)致可讀性差,調(diào)試難度净赴。
建議:
? 盡量少地使用成員變量绳矩,變量的作用范圍控制到最小
- api使用困難
由于函數(shù)和類成員變量偶合,在調(diào)用某個函數(shù)之前可能要先調(diào)用其它函數(shù)玖翅,如果順序不對翼馆,就可能導(dǎo)致異常。
建議:
-[x] 盡量使每個函數(shù)不引用成員變量和全局變量烧栋,使函數(shù)無副作用写妥,引用透明。
-[x] 界面類只是簡單地響應(yīng)用戶事件及顯示數(shù)據(jù)审姓,盡量不要有邏輯。
? 看看維基百科中聲明式編程的定義:
聲明式編程是告訴計算機需要計算“什么”而不是“如何”去計算
任何沒有副作用的編程語言狡刘,或者更確切一點享潜,任何引用透明的編程語言
-
任何有嚴(yán)格計算邏輯的編程語言
?
我個人理解,這三個特點中的前兩個都是為了模仿人類的思維方式嗅蔬。
?
因為人類思考主要靠直覺剑按,人類自己也搞不清楚自己怎么想出答案的。所以聲明式編程語言也不要規(guī)定電腦具體怎么執(zhí)行命令澜术。
因為人類很固執(zhí)艺蝴,腦海中已有的概念很難修改。所以聲明式編程也要模仿人類瘪板,不允許修改變量吴趴。
第三點則是人工智能先驅(qū)的美好愿望,像人類一樣思考侮攀,但是別像人類一樣犯錯锣枝。
抽象方式
-
面向?qū)ο笾g需要協(xié)作,導(dǎo)致對象之間關(guān)系錯綜復(fù)雜兰英,不好理清撇叁。
建議:
-[x] 可以將功能抽象成對象,但抽象的基礎(chǔ)是數(shù)據(jù)對象的映射畦贸,每個對象只實現(xiàn)自己的功能陨闹,和其它對象沒有任何耦合。
-[x] 流程圖上的一個節(jié)點抽象成一個對象薄坏,最終有一個對象或者函數(shù)負(fù)責(zé)將所有的對象連接在一起趋厉,完成整個流程。這時候胶坠,可以從這個對象或者函數(shù)可以清晰地看到整個流程
三大特性
-
封裝
- 可讀性差君账、調(diào)試?yán)щy
面向?qū)ο笏枷雽⒆兞糠庋b到類里,然后使用函數(shù)對其進行操作沈善,成員變量的作用域在整個類乡数,需要考慮變量隨著時間的變化椭蹄。導(dǎo)致可讀性差,調(diào)試難度净赴。
建議:
? 盡量少地使用成員變量绳矩,變量的作用范圍控制到最小
- api使用困難
由于函數(shù)和類成員變量偶合,在調(diào)用某個函數(shù)之前可能要先調(diào)用其它函數(shù)玖翅,如果順序不對翼馆,就可能導(dǎo)致異常。
建議:
-[x] 盡量使每個函數(shù)不引用成員變量和全局變量烧栋,使函數(shù)無副作用写妥,引用透明。
-[x] 界面類只是簡單地響應(yīng)用戶事件及顯示數(shù)據(jù)审姓,盡量不要有邏輯。
?
-
繼承
需要理解繼承層次間類的關(guān)系祝峻,可讀性差魔吐,繼承層次大于3層時,就非常難理解莱找。
使變量和函數(shù)的作用域擴大酬姆,使耦合度變大,可讀性進一步降低奥溺。
使函數(shù)的作用域擴大辞色,導(dǎo)致一些函數(shù)覆蓋問題。
建議:
-[x] 不使用類
-[x] 少用繼承浮定,或者不繼承相满,使用了繼承,層次盡量不要超過3層
-[x] 多使用組合桦卒,少用繼承
-
多態(tài)
- 調(diào)試?yán)щy立美,需要運行時,才能確定具體哪個類實現(xiàn)了哪個函數(shù)方灾。
建議:
-[x] 使用高階函數(shù)和柯里化實現(xiàn)差異化
為什么函數(shù)式編程這么多優(yōu)點建蹄,沒有流行起來
- 剛開始只是為了數(shù)學(xué)計算,有些場景無法實現(xiàn)裕偿,因此不適合實際工作應(yīng)用洞慎,特別是對界面的處理。不過也有用函數(shù)式實現(xiàn)界面的嘿棘,比如om
- 由于大量使用遞歸劲腿,那時候的編譯器沒做優(yōu)化,導(dǎo)致運行緩慢蔫巩。
不僅最古老的函數(shù)式語言Lisp重獲青春谆棱,而且新的函數(shù)式語言層出不窮快压,比如Erlang、clojure垃瞧、Scala蔫劣、F#等等。目前最當(dāng)紅的Swift个从、C++脉幢、Objective-C、C#嗦锐、Python嫌松、Ruby、Javascript奕污,對函數(shù)式編程的支持都很強萎羔,就連老牌的面向?qū)ο蟮腏ava、面向過程的PHP碳默,都忙不迭地加入對匿名函數(shù)的支持贾陷。越來越多的跡象表明,函數(shù)式編程已經(jīng)不再是學(xué)術(shù)界的最愛嘱根,開始大踏步地在業(yè)界投入實用髓废。
也許繼"面向?qū)ο缶幊?之后,"函數(shù)式編程"會成為下一個編程的主流范式(paradigm)该抒。未來的程序員恐怕或多或少都必須懂一點慌洪。
java也在努力的改革,進步凑保,在像函數(shù)式“進化”冈爹,比如說java8提供的Stream流,lambda愉适,努力將函數(shù)(方法)提升為一等公民犯助,Stream中的透明化,無態(tài)化都流露著函數(shù)式的思想维咸。雖然java的fp現(xiàn)在可能走得是oop的極端表現(xiàn)形式剂买,但是也從另一個側(cè)面表達了fp的優(yōu)點和將來大勢所在。
作者:Accelerator鏈接:https://www.zhihu.com/question/30190384/answer/142902047來源:知乎著作權(quán)歸作者所有癌蓖。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)瞬哼,非商業(yè)轉(zhuǎn)載請注明出處。
現(xiàn)在流行起來租副,個人認(rèn)為有以下幾個原因:
- 計算機硬件坐慰,多核技術(shù)發(fā)展使性能不再是瓶頸
- fp編譯器的進步
- 移動互聯(lián)網(wǎng)時代,分布式用僧、并發(fā)應(yīng)用程序的時代終于來臨了
- 開源庫ReactiveCocoa结胀,RxJava及前端Redux使用了大量函數(shù)式的編程思想赞咙。
- 大數(shù)據(jù),人工智能的發(fā)展導(dǎo)致只對數(shù)據(jù)進行處理糟港。