本文主要講了我自己對函數(shù)式編程的理解。涉及到編程范式的方面帽氓,個人的理解不免有遺漏、不準確甚至錯誤的地方俩块,希望能多多批評黎休、交流、指正玉凯。
我們認識事物势腮,總是要問一下「What」「Why」「How」。那么漫仆,ReactiveCocoa是什么呢捎拯?
說到ReactiveCocoa,相信大部分看過介紹的人都會看到一句開場白:
ReactiveCocoa是一個函數(shù)式響應(yīng)式的編程框架盲厌。
WTF署照,這個函數(shù)式響應(yīng)式編程
是個什么鬼?這一篇文章吗浩,希望從我的理解出發(fā)建芙,先來聊聊函數(shù)式編程。
函數(shù)式編程是什么
介紹函數(shù)式編程的文章拓萌,一般都逃不過這句話:
函數(shù)式編程中,函數(shù)是一等公民升略。(Functions are first-class objects)
怎么去理解這句話呢微王?我的理解是,無論是函數(shù)式編程中的「函數(shù)是一等公民」品嚣,還是面向?qū)ο笫骄幊讨械摹溉f物皆對象」炕倘,都是對事物的不同抽象方式。
在面向?qū)ο笫降木幊讨校?code>類和對象
的概念深入人心翰撑。我們的類是「人」罩旋,阿貓阿狗的類是「寵物」啊央,我們都有一個父類「動物」(這些話在每本面向?qū)ο笳Z言的入門書籍中都會出現(xiàn))。而行為
必須依賴于類
或對象
涨醋,比如「吃東西」是「動物」的一個行為瓜饥,所以子類「人」和「寵物」都會吃東西。尋找事物的共同點浴骂,并把它們歸類乓土,面向?qū)ο笠赃@種抽象方式來描述這個世界。
而函數(shù)式編程則換了一種思路:它是對「行為」的抽象溯警。在函數(shù)式編程里趣苏,「吃東西」就是「吃東西」,輸入一些食物梯轻,輸出一些經(jīng)過加工的結(jié)果(咳咳食磕,這里就不具體說是什么了……),這是多么純粹的一個行為喳挑。既然只涉及到對食物(數(shù)據(jù))的處理這一行為彬伦,那為什么需要具體的類呢?類似的還有對「睡覺」蟀悦,「打豆豆」……等等行為的抽象媚朦。這些對行為的抽象是函數(shù)式編程對這個世界的描述方式。
既然是一等公民日戈,那么自然地询张,函數(shù)式編程中,函數(shù)可以以參數(shù)浙炼、返回值的形式從一個函數(shù)傳遞到另一個函數(shù)份氧,也就產(chǎn)生了高階函數(shù)。這種傳遞本質(zhì)上是對數(shù)據(jù)的流式處理弯屈。
舉個栗子蜗帜,「計算一個數(shù)乘以3的結(jié)果」,這是一個相當純粹的行為吧资厉,我們很容易寫出這樣的函數(shù):
// 代碼1
- (NSNumber *)multipliedByThree:(NSNumber *)x {
return @(x.integerValue * 3);
}
輸入一個參數(shù)厅缺,輸出一個結(jié)果,平平無奇的一個函數(shù)哈宴偿。但如果剛好有另外一個函數(shù)湘捎,它能夠?qū)σ粋€集合里的所有元素進行一個操作,然后返回一個新集合:
// 代碼2
@implementation NSArray(SomeExcute)
- (NSArray *)map:(id (^)(id obj))block
{
NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count];
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
id value = block(obj) ?: [NSNull null];
[result addObject:value];
}];
return result;
}
那么窄刘,我們就可以將multipliedByThree
這個函數(shù)和map
這個函數(shù)進行一次激情的碰撞:
// 代碼3
[@[@(1), @(2), @(3), @(4)] map:^id(id x){
return @(x.integerValue * 3);
}];
瞬間就將原來的數(shù)組變成了另外一個經(jīng)過處理的數(shù)組了窥妇,干脆利落。
雖然代碼很簡單娩践,但是還是可以稍作分析:multipliedByThree
函數(shù)是我們對一個處理數(shù)據(jù)的「行為」的抽象活翩,map
也是我們對一個處理數(shù)據(jù)(數(shù)組或其他集合)的「行為」的抽象烹骨,同時,map
是一個高階函數(shù):它接受另一個函數(shù)作為參數(shù)材泄。通過multipliedByThree
和map
的抽象沮焕,我們得以在代碼3
中,使用簡明干練的語法和代碼脸爱,完成對一個集合的復(fù)雜處理:我們需要返回一個新數(shù)組遇汞,它的每一個元素值都是原數(shù)組中對應(yīng)值的3倍。
注意上面最后一句話簿废,這是很直觀的一個「聲明式」的語句空入。再回頭看看上面的代碼3
,你會發(fā)現(xiàn)這段代碼也充滿了「聲明式」的意味族檬。我們在這里只是告訴計算機歪赢,我們「需要的」是什么,而不是去描述我們應(yīng)當「怎么去做」(遍歷集合中的每個元素单料,將該值乘以3埋凯,并添加到一個新的集合中,最后返回這個新的集合)扫尖。
這正是函數(shù)式編程的魅力之一:它將原本的命令式編程(Imperative)
變成了聲明式編程(Declarative)
白对,將「How to do」變成了「What we want」,將思考「如何把這個問題用代碼實現(xiàn)」轉(zhuǎn)變成了「如何用代碼去描述這個問題」换怖,從而使得代碼更加簡潔明了和具有自注釋性甩恼。諸如map
的函數(shù)正是對「行為」的抽象,而通過抽象出若干個更小的沉颂、更通用的行為
条摸,將細節(jié)(如iteration)隱藏在了抽象中,我們外部的代碼也就具有了「聲明式」的性質(zhì)铸屉。
此外钉蒲,由于有了map
的抽象和其接受另一個函數(shù)為參數(shù)的高階函數(shù)特性,我們能夠?qū)⒉煌饔玫暮瘮?shù)應(yīng)用于同一份map
代碼上彻坛,來實現(xiàn)對數(shù)據(jù)的不同的處理顷啼,從而使得代碼的功能結(jié)構(gòu)更加清晰,模塊性和可復(fù)用性大大增加昌屉。這就好比同樣的原材料钙蒙,同樣的一臺車床,只要更換不同的刀頭怠益,我們就能做出不一樣的成品來仪搔。
對于上面這幾段代碼瘾婿,還有兩點想補充解釋:
- 這里的
map
函數(shù)表現(xiàn)為NSArray
的一個方法蜻牢。但是烤咧,從本質(zhì)上說,這個操作是可以普遍存在的抢呆,不依賴于這一個類煮嫌。下文對此會有更詳細的描述。 -
map
函數(shù)的內(nèi)部實現(xiàn)使用了循環(huán)抱虐。這里對于在函數(shù)式編程中遍歷集合中的元素是否能使用循環(huán)我尚有疑問昌阿,但是函數(shù)式編程中的函數(shù)內(nèi)部,應(yīng)當是使用遞歸而不是循環(huán)恳邀。遞歸也是「自解釋性」的懦冰,「聲明式」的,而循環(huán)則是「命令式」的代碼谣沸。更重要的是刷钢,循環(huán)是會使用到變量和狀態(tài),而這正是函數(shù)式編程想要消除的(下文會講到)乳附。
「一等公民」許可證
In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. -- wiki
以上這段話是wiki中函數(shù)式編程的定義内地。在這段話中,有兩個地方值得注意:
首先赋除,「mathematical functions」阱缓,即數(shù)學意義上的函數(shù)意味著什么呢?它意味著一個函數(shù)要想成為函數(shù)式編程中的一等公民举农,可是有門檻的——它必須是純函數(shù)(pure function)
荆针。
來看這樣一段代碼:
// 代碼4
@interface ClassA : NSObject
@property (readwrite) NSInteger count;
@end
@implementation ClassA
- (NSInteger)doAdd:(NSInteger)x {
return self.count + x;
}
@end
其中的doAdd
函數(shù)的返回值,毫無疑問是依賴于外部的變量count
的并蝗。當count
的值發(fā)生變化時祭犯,即使入?yún)?code>x的值不變,該函數(shù)也會返回一個完全不同的值滚停。而這是純函數(shù)絕不容許發(fā)生的:在數(shù)學上沃粗,一個函數(shù)的返回值是完全取決于輸入的參數(shù)的,即對于相同的參數(shù)键畴,必然會產(chǎn)生相同的結(jié)果最盅。這就是純函數(shù)所必須的第一個要求——確定性。
其次起惕,「avoids changing-state and mutable data」說的則是“一等公民”的另一個重要的特點——純函數(shù)必須不能產(chǎn)生副作用
涡贱。副作用
是指一個函數(shù)在運行過程中對函數(shù)外部世界的影響,比如修改外部的變量(changing-state)惹想,改變可變數(shù)據(jù)(mutable data)问词,或者進行io操作等等。此外嘀粱,由于「循環(huán)」操作需要依靠循環(huán)變量作為中間值激挪,所以在純函數(shù)中辰狡,循環(huán)也是不被允許的,其可被「遞歸」代替垄分。而遞歸所產(chǎn)生的調(diào)用棧太深的問題宛篇,可以由「尾遞歸優(yōu)化」解決,這里不多深入探討了薄湿。
一句話總結(jié)叫倍,純函數(shù)必須只完成自己的工作,對外部世界一無所知豺瘤,既不依賴于外部世界吆倦,也不會對外部世界產(chǎn)生影響。
函數(shù)式編程中的「狀態(tài)」和「副作用」
在命令式編程
中坐求,「狀態(tài)」和「變量」算是不能再常見的東西了逼庞。當我們需要一步步編寫一條條命令的時候,必須得有臨時變量或者全局變量來保存上一步的操作結(jié)果或者狀態(tài)瞻赶,以便下一步的命令使用赛糟。最常見的就是「賦值」操作了:我們將一個中間的數(shù)據(jù)或狀態(tài)賦值給一個變量,然后在以后的某個時刻去使用它砸逊。而「狀態(tài)」和「副作用」也確實是一個真正的工程項目所不可或缺的璧南。真的會有工程項目不需要進行io操作嗎?但是像上文所說的师逸,如果函數(shù)式編程中的函數(shù)必須是純函數(shù)
的話司倚,該怎么處理這些呢?
函數(shù)式編程并不是完全拋棄了它們篓像,只不過动知,函數(shù)式編程是通過參數(shù)的傳遞來完成這一切的。將「狀態(tài)」(即「上下文」)和「副作用」包裹在參數(shù)中员辩,在真正核心的數(shù)據(jù)傳遞的時候盒粮,將這些必不可少的信息同時打包傳遞下去,這樣就可以在保證函數(shù)的純潔性的同時奠滑,保存數(shù)據(jù)處理所必需的上下文信息丹皱。這就好比將真正需要處理的數(shù)據(jù)和必要的上下文信息,副作用等封裝在了一個盒子里宋税,然后讓它在一個個純函數(shù)的“管道”中流通摊崭、處理,不需要中間變量杰赛,也不需要全局狀態(tài)呢簸。數(shù)據(jù)就這樣“腳不沾地”地一步步變成了我們最終需要的模樣。
但是,橋豆麻袋根时!想象一下嘿架,如果各個“管道”所進行的操作不一樣:有的僅僅會返回一個值類型,有的會將參數(shù)的盒子打開啸箫,處理之后再返回一個新的“盒子”類型……此外,如果在每個管道里都要進行打開盒子-->取出核心的數(shù)據(jù)-->處理-->然后再將一些「副作用」等等一起封裝成一個新的盒子-->傳遞出去這樣一個流程的話伞芹,那么也太繁瑣了點忘苛。其實,這些都是函數(shù)式編程中另一個重要的概念——monad
所要解決的事情唱较。
monad
的概念google一下可謂眾說紛紜扎唾。在這里,我覺得可以將其理解為函數(shù)式編程中一個在函數(shù)間約定好的數(shù)據(jù)類型南缓。用Objective-C的語言來說胸遇,它是一個實現(xiàn)了monad protocol
的“盒子”類型。monad protocol
定義了如下兩個接口函數(shù):
-(BoxValue)return:(Value)value;
-(BoxValue)bind:(Value->BoxValue)f;
而且汉形,monad protocol
是一個派生接口纸镊,其派生關(guān)系為Functor->Applicative->Monad
,因此概疆,可以認為monad protocol
中還繼承了下列兩個接口函數(shù):
// 繼承自Functor Protocol
-(BoxValue)map:(Value->Value)f;
// 繼承自Applicative Protocol
-(BoxValue)applied:(BoxValue(Value->Value)f); //applied也被稱為<*>
其中逗威,return
函數(shù)解決的是“如何封裝”的問題:接收一個值類型的數(shù)據(jù),返回一個當前的“盒子”類型的數(shù)據(jù)(封裝后的值)岔冀;map
和bind
函數(shù)(也被稱為>>=)解決的是“如何傳遞”的問題:接收一個“由值類型到值類型”或“由值類型到盒子類型”的數(shù)據(jù)處理的函數(shù)凯旭,返回一個當前的“盒子”類型的數(shù)據(jù)。
(注:這里只是簡單說明了monad
的概念使套,感興趣的可以深入閱讀參考資料中的相關(guān)內(nèi)容)
實現(xiàn)了上述兩個接口函數(shù)的數(shù)據(jù)結(jié)構(gòu)就是monad
罐呼。根據(jù)其中封裝的「副作用」的不同,產(chǎn)生了各種各樣的monad
侦高,比如可以包含若干可能類型的值的Maybe
或者進行io操作的io monad
嫉柴;而有了map
和bind
函數(shù)來保證管道前后的數(shù)據(jù)類型的一致性,我們則可以玩出這樣的花樣:
// 代碼5
[[[[BoxValue bind:block1]
bind:block2]
map:block3]
bind:block4];
我們不僅可以像這樣協(xié)調(diào)串聯(lián)起各個不同類型奉呛,不同作用的函數(shù)(block1
差凹,block2
,block3
侧馅,block4
)所組成的整個傳遞鏈危尿,使得數(shù)據(jù)可以在這樣的鏈式操作中進行一步步地操作,而且在傳遞的過程中還可以由monad
自動處理上下文的信息馁痴,使得我們可以專注于處理真正的值谊娇。這就很厲害了,可以說monad
是函數(shù)式編程可以進行鏈式操作(有的地方也叫流式操作)的基礎(chǔ)。
回過頭看我們上面的代碼2
和代碼3
济欢,發(fā)現(xiàn)什么端倪了嗎赠堵?沒錯,代碼2
就是「數(shù)組」這個數(shù)據(jù)類型對map
函數(shù)的實現(xiàn)法褥,而代碼3
則是“一段管道”茫叭,所以,在這里「數(shù)組」就是一個Functor
了半等。
而我們這個系列的主角——ReactiveCocoa揍愁,也是建立在一個monad
的基礎(chǔ)上的——RACStream
類及其子類,這個monad
封裝了「異步」的副作用杀饵,從而使得函數(shù)式響應(yīng)式的編程得以實現(xiàn)莽囤,當然這是后話了,在后續(xù)系列的文章中會詳盡闡述切距。
總結(jié)
函數(shù)式編程的優(yōu)點總結(jié)起來大概有這么幾點:
- 如上文所說朽缎,函數(shù)式編程中通過抽象出函數(shù),從而隱藏了實現(xiàn)的細節(jié)谜悟,使得代碼更具有「聲明性」话肖,邏輯更加清晰。此外葡幸,函數(shù)的抽象還有利于邏輯層面的代碼復(fù)用狼牺。
- 由于函數(shù)式編程中的函數(shù)為「純函數(shù)」,對于輸入輸出具有確定性礼患,所以可測試性是很強的是钥,調(diào)試的時候也會更加方便。
- 由于函數(shù)式編程中的函數(shù)對外部世界一無所知缅叠,既不依賴于外部世界悄泥,也不會對外部世界產(chǎn)生影響,所以更加適合并發(fā)編程肤粱,不用到處使用線程鎖來手動保證線程安全弹囚。
函數(shù)式編程也有一些劣勢:
- 函數(shù)式編程的認知成本比較大,學習曲線比較陡峭领曼,至少對于我來說是這樣的(sigh~)鸥鹉。
- 有些真實客觀存在的事物確實不是函數(shù)式編程的建模方式所擅長描述的,函數(shù)式編程要想描述出這些事物可能會比面向?qū)ο笫骄幊桃@得復(fù)雜和拙劣庶骄。
這或許就是我們使用(不用)函數(shù)式編程的原因吧毁渗。
在我看來,編程范式并不是一個統(tǒng)一的標準单刁。函數(shù)式編程具有的數(shù)學上的嚴謹?shù)拿栏芯囊欤约疤幚頂?shù)據(jù)具有的優(yōu)勢,使得它在應(yīng)用程序的底層的數(shù)據(jù)處理,或是科學研究中的數(shù)據(jù)處理方面有著很大的價值肺樟;而面向?qū)ο笫骄幊虅t也許會在GUI編程中發(fā)揮更大的作用檐春。
此外,現(xiàn)在很多語言也引入了函數(shù)式編程的特性和風格么伯,例如js疟暖,ruby,python等等田柔。在代碼的編寫中我們大可以充分靈活地運用這兩種編程思想各自的優(yōu)點俐巴,畢竟,代碼還是為人服務(wù)的嘛凯楔。
Reference
[Wikipedia: Functional Programming][2]
[2]: https://en.wikipedia.org/wiki/Functional_programming
[Why Functional Programming Matters][3]
[3]: http://www.cs.utexas.edu/~shmat/courses/cs345/whyfp.pdf
[Functional Thinking: Why functional programming is on the rise][4]
[4]: https://www.ibm.com/developerworks/java/library/j-ft20/index.html
[Functional Programming for JavaScript People][5]
[5]: https://medium.com/@chetcorcos/functional-programming-for-javascript-people-1915d8775504
[函數(shù)式編程掃盲篇][6]
[6]: http://blog.csdn.net/jiajiayouba/article/details/49983325
[Don’t Be Scared Of Functional Programming][7]
[7]: https://www.smashingmagazine.com/2014/07/dont-be-scared-of-functional-programming/
[Functor and Monad in Swift][8]
[8]: http://www.javiersoto.me/post/106875422394
[揭開 Monad 的神秘面紗][9]
[9]: http://joeyio.com/2016/04/24/RevealMonad/
[Functor、Applicative 和 Monad][10]
[10]: http://blog.leichunfeng.com/blog/2015/11/08/functor-applicative-and-monad/
[怎樣用簡單的語言解釋 monad][11]
[11]: https://www.zhihu.com/question/24972880
[Imperative and Declarative Programming][12]
[12]: http://theproactiveprogrammer.com/design/imperative-and-declarative-programming/?utm_source=tuicool&utm_medium=referral
[函數(shù)式編程--CoolShell][13]
[13]: https://coolshell.cn/articles/10822.html