首先來了解一下函數(shù)式反應型編程Functional Reactive Programming
函數(shù)式反應型編程是兩個聲明式編程的子范例(函數(shù)式
+反應式
)的組合
函數(shù)式編程
編程范式(Programming paradigm)
分類
其實就是計算機編程所使用的方法什猖,是設計程序結構所采用的設計風格盏阶。
目前主流的編程范式有:
-
命令式編程(Imperative programming)
- 如Pascal统阿,C語言
- First DO THIS and next DO THAT
- 其他統(tǒng)稱為
聲明式編程(Declarative Programming)
-
函數(shù)式編程(Functional programming)
- 如Haskell,Erlang其徙, Lisp
- Evaluate an expression and use the resulting value for something
-
面向對象編程(Object-oriented programming)
- 如Java归苍,C++
- Send messages between objects to simulate the temporal evolution of a set of real world phenomena
-
邏輯編程(Logic Programming)
- 如Prolog吮便,Mercury黔寇,Logtalk
- Answer a question via search for a solution
函數(shù)式編程特點
外鏈參考:函數(shù)式編程初探
- 函數(shù)是"第一等公民"。指的是函數(shù)與其他數(shù)據(jù)類型一樣觉壶,處于平等地位脑题,可以賦值給其他變量,也可以作為參數(shù)铜靶,傳入另一個函數(shù)叔遂,或者作為別的函數(shù)的返回值。
- 只用"表達式"(expression)争剿,不用"語句"(statement)已艰。函數(shù)式編程要求,只使用表達式秒梅,不使用語句旗芬。也就是說舌胶,每一步都是單純的運算捆蜀,而且都有返回值。
- 沒有"副作用"(side effect)幔嫂。意味著函數(shù)要保持獨立辆它,所有功能就是返回一個新的值,沒有其他行為履恩,尤其是不得修改外部變量的值锰茉。
- 不修改狀態(tài)
- 引用透明(Referential transparency)。指的是函數(shù)的運行不依賴于外部變量或"狀態(tài)"切心,只依賴于輸入的參數(shù)飒筑,任何時候只要參數(shù)相同片吊,引用函數(shù)所得到的返回值總是相同的。
高階函數(shù)
函數(shù)式編程的一個關鍵的概念是"高階函數(shù)"协屡。從維基百科的解釋來看俏脊,一個高階函數(shù)需要滿足下面兩個條件:
- 一個或者多個函數(shù)作為輸入。
- 有且僅有一個函數(shù)輸出肤晓。
在Objective-c中我們經常使用block作為函數(shù)爷贫。我們不需要跋山涉水地去尋找‘高階函數(shù)’,實際上补憾,Apple為我們提供的Foundation庫中就有漫萄。考慮象下面這么簡單的一個NSNumber 的數(shù)組:
NSArray * array = @[ @(1), @(2), @(3) ];
我們想要枚舉這個數(shù)組的內容盈匾,可以用一個NSArray的高階函數(shù)來實現(xiàn):
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop)
{
NSLog(@"%@",number);
}];
函數(shù)式編程意義
- 代碼簡潔腾务,開發(fā)快速
- 接近自然語言,易于理解
- 更方便的代碼管理
- 易于"并發(fā)編程"
函數(shù)式編程和遞歸
外鏈參考:[函數(shù)式編程掃盲篇](http://www.cnblogs.com/kym/archive/2011/03/07/1976519.html
遞歸是函數(shù)式編程的一個重要的概念威酒,循環(huán)可以沒有窑睁,但是遞歸對于函數(shù)式編程卻是不可或缺的。
循環(huán)是在描述我們該如何地去解決問題葵孤。
遞歸是在描述這個問題的定義担钮。
考慮經典的斐波那契數(shù)列問題1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …,我們很容易從數(shù)列本身的定義得到一個遞推式:f(n)=f(n-1)+f(n-2)
:
先看循環(huán)模型:
def Fib(n):
a=1
b=1
n = n - 1
while n>0:
temp=a
a=a+b
b=temp
n = n-1
return b
遞歸模型:
def Fib(a):
if a==0 or a==1:
return 1
else:
return Fib(a-2)+Fib(a-1)
尾遞歸模型:
def Fib(a,b,n):
if n==0:
return b
else:
return Fib(b,a+b,n-1)
模擬
a, b, n
Fib (1, 1, 10)
Fib (1, 2, 9)
Fib (2, 3, 8)
什么是尾遞歸
尤仍,用最通俗的話說:就是在最后一部單純地去調用遞歸函數(shù)箫津,這里我們要注意“單純”這個字眼。
那么我們說下尾遞歸的原理宰啦,其實尾遞歸就是不要保持當前遞歸函數(shù)的狀態(tài)苏遥,而把需要保持的東西全部用參數(shù)給傳到下一個函數(shù)里,這樣就可以自動清空本次調用的椛哪#空間田炭。這樣的話,占用的椑旄蹋空間就是常數(shù)階的了教硫。
在看尾遞歸代碼之前,我們還是先來明確一下遞歸的分類辆布,我們將遞歸分成“樹形遞歸”和“尾遞歸”瞬矩,什么是樹形遞歸,就是把計算過程逐一展開锋玲,最后形成的是一棵樹狀的結構景用,比如之前的斐波那契數(shù)列的遞歸解法。
響應式編程
看下面一段代碼
a = 2
b = 2
c = a + b // c 是 4
b = 3 // 現(xiàn)在c是多少?
在命令式編程語言中惭蹂,c = a + b
這個語句一旦執(zhí)行完畢伞插,a 和 b 再發(fā)生變化就和 c 無關了割粮,c 并不會跟著變化。如果需要 c 變化時媚污,我們一般會封裝一個類似 update_c() 這樣的函數(shù)穆刻,在 a 或 b 變化的時候調用一下,來更新c杠步。
而在響應式編程的思想中氢伟,上面的語句實際上是建立了 c 和 a、b 的關聯(lián)關系幽歼,這樣朵锣,當 a 或 b 發(fā)生變化的時候,c 可以自動變化甸私。
Excel就是響應式編程的一個例子诚些。單元格可以包含字面值或類似”=B1+C1″的公式,而包含公式的單元格的值會依據(jù)其他單元格的值的變化而變化 皇型。
注意這只是一個思想诬烹,或者說是目標,我們可以使用個各種語言來實現(xiàn)這個目標弃鸦,并不是說必須要有一種專門的響應型編程語言
绞吁。當然,語言可以根據(jù)這個思路來設計唬格,會讓響應型編程的實現(xiàn)更為簡單家破。
kvo
, Cocoa Binding
,
ReactiveCocoa
外鏈: ReactiveCocoa iOS的函數(shù)響應型編程 ReactiveCocoa for a better world ReactiveCocoa v2.5 源碼解析之架構總覽
ReactiveCocoa是函數(shù)式響應型編程的一個實現(xiàn)。它受 Functional Reactive Programming 的啟發(fā)购岗,是 Justin Spahr-Summers 和 Josh Abernathy 在開發(fā) GitHub for Mac 過程中的一個副產品汰聋,它提供了一系列用來組合和轉換值流的 API 。
ReactiveCocoa 的版本演進歷程喊积,簡單介紹如下:
- <= v2.5 :Objective-C 烹困;
- v3.x :Swift 1.2 ;
- v4.x :Swift 2.x 乾吻。
本文所介紹的均為 ReactiveCocoa v2.5 版本中的內容髓梅,這是 Objective-C 最新的穩(wěn)定版本。
ReactiveCocoa類圖如下圖所示
ReactiveCocoa 主要由以下四大核心組件構成:
- 流:
RACStream
及其子類溶弟; - 訂閱者:
RACSubscriber
的實現(xiàn)類及其子類女淑; - 調度器:
RACScheduler
及其子類瞭郑; - 清潔工:
RACDisposable
及其子類辜御。
流RACStream
在ReactiveCocoa中,流RACStream
代表的是隨著時間而改變的值流(Streams of values over time)屈张。
你可以把它想象成水龍頭中的水擒权,當你打開水龍頭時袱巨,水源源不斷地流出來;你也可以把它想象成電碳抄,當你插上插頭時愉老,電靜靜地充到你的手機上;你還可以把它想象成運送玻璃珠的管道剖效,當你打開閥門時嫉入,珠子一個接一個地到達。這里的水璧尸、電咒林、玻璃珠就是我們所需要的值,而打開水龍頭爷光、插上插頭垫竞、打開閥門就是訂閱它們的過程。
RACStream 是一個抽象類蛀序,通常情況下欢瞪,我們并不會去實例化它,而是直接使用它的兩個子類信號RACSignal
和序列RACSequence
徐裸。
信號RACSignal
RACSignal
代表的是未來將會被傳送的值遣鼓,它是一種push-driven
的流。
信號又是最核心的部分重贺,其他組件都是圍繞它運作的譬正。
對于一個應用來說,絕大部分的時間都是在等待某些事件的發(fā)生或響應某些狀態(tài)的變化檬姥,比如用戶的觸摸事件曾我、應用進入后臺、網絡請求成功刷新界面等等健民,而維護這些狀態(tài)的變化抒巢,常常會使代碼變得非常復雜,難以擴展秉犹。而 ReactiveCocoa
給出了一種非常好的解決方案蛉谜,它使用信號來代表這些異步事件,提供了一種統(tǒng)一的方式來處理所有異步的行為崇堵,包括代理方法型诚、block
回調、target-action 機制
鸳劳、通知狰贯、KVO
等:
// 代理方法
[[self
rac_signalForSelector:@selector(webViewDidStartLoad:)
fromProtocol:@protocol(UIWebViewDelegate)]
subscribeNext:^(id x) {
// 實現(xiàn) webViewDidStartLoad: 代理方法
}];
// target-action
[[self.avatarButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(UIButton *avatarButton) {
// avatarButton 被點擊了
}];
// 通知
[[[NSNotificationCenter defaultCenter]
rac_addObserverForName:kReachabilityChangedNotification object:nil]
subscribeNext:^(NSNotification *notification) {
// 收到 kReachabilityChangedNotification 通知
}];
// KVO
[RACObserve(self, username) subscribeNext:^(NSString *username) {
// 用戶名發(fā)生了變化
}];
然而,這些還只是 ReactiveCocoa 的冰山一角,它真正強大的地方在于我們可以對這些不同的信號進行任意地組合和鏈式操作涵紊,從最原始的輸入 input 開始直至得到最終的輸出 output 為止:
[[[RACSignal
combineLatest:@[ RACObserve(self, username), RACObserve(self, password) ]
reduce:^(NSString *username, NSString *password) {
return @(username.length > 0 && password.length > 0);
}]
distinctUntilChanged]
subscribeNext:^(NSNumber *valid) {
if (valid.boolValue) {
// 用戶名和密碼合法傍妒,登錄按鈕可用
} else {
// 用戶名或密碼不合法,登錄按鈕不可用
}
}];
因此摸柄,對于 ReactiveCocoa 來說颤练,我們可以毫不夸張地說,阻礙它發(fā)揮的瓶頸就只剩下你的想象力了驱负。
RACSignal
可以向訂閱者發(fā)送三種不同類型的事件:
-
next
:RACSignal
通過next
事件向訂閱者傳送新的值嗦玖,并且這個值可以為 nil ; -
error
:RACSignal
通過error
事件向訂閱者表明信號在正常結束前發(fā)生了錯誤跃脊; -
completed
:RACSignal
通過completed
事件向訂閱者表明信號已經正常結束踏揣,不會再有后續(xù)的值傳送給訂閱者。
注意匾乓,ReactiveCocoa 中的值流只包含正常的值捞稿,即通過next
事件傳送的值,并不包括error
和completed
事件拼缝,它們需要被特殊處理娱局。通常情況下,一個信號的生命周期是由任意個next
事件和一個error
事件或一個completed
事件組成的咧七。
從前面的類圖中衰齐,我們可以看出,RACSignal
并非只有一個類继阻,事實上耻涛,它的一系列功能是通過類簇來實現(xiàn)的。除去我們將在下節(jié)介紹的 RACSubject
及其子類外瘟檩,RACSignal
還有五個用來實現(xiàn)不同功能的私有子類:
-
RACEmptySignal
:空信號抹缕,用來實現(xiàn)RACSignal
的+empty
方法; -
RACReturnSignal
:一元信號墨辛,用來實現(xiàn)RACSignal
的+return:
方法卓研; -
RACDynamicSignal
:動態(tài)信號,使用一個block
來實現(xiàn)訂閱行為睹簇,我們在使用RACSignal
的+createSignal:
方法時創(chuàng)建的就是該類的實例奏赘; -
RACErrorSignal
:錯誤信號,用來實現(xiàn)RACSignal
的+error:
方法太惠; -
RACChannelTerminal
:通道終端磨淌,代表RACChannel
的一個終端,用來實現(xiàn)雙向綁定凿渊。
對于RACSignal
類簇來說梁只,最核心的方法莫過于-subscribe:
了缚柳,這個方法封裝了訂閱者對信號源的一次訂閱過程,它是訂閱者與信號源產生聯(lián)系的唯一入口敛纲。因此,對于RACSignal
的所有子類來說剂癌,這個方法的實現(xiàn)邏輯就代表了該子類的具體訂閱行為淤翔,是區(qū)分不同子類的關鍵所在。同時佩谷,這也是為什么RACSignal
中的-subscribe:
方法是一個抽象方法旁壮,并且必須要讓子類實現(xiàn)的原因:
- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
NSCAssert(NO, @"This method must be overridden by subclasses");
return nil;
}
序列RACSequence
RACSequence
代表的是一個不可變的值的序列,與RACSignal
不同谐檀,它是pull-driven
類型的流抡谐。從嚴格意義上講,RACSequence
并不能算作是信號源桐猬,因為它并不能像RACSignal
那樣麦撵,可以被訂閱者訂閱,但是它與RACSignal
之間可以非常方便地進行轉換溃肪。序列提供了Foundation沒有的一些高階函數(shù)如map
, filter
, fold
等免胃。
使用rac_sequeuece
我們能夠輕松地將數(shù)組轉化為一個序列:
NSArray *array = @[ @1, @2, @3 ];
RACSequence * stream = [array rac_sequence];
我們可以將流應用在平方數(shù)映射上,然后轉化回一個數(shù)組:
[stream map:^id (id value){
return @(pow([value integerValue], 2));
}];
NSLog(@"%@",[stream array]);
當然惫撰,我們可以合并上面的方法調用來避免污染變量的作用域.
NSLog(@"%@",[[[array rac_sequence] map:^id (id value){
return @(pow([value integerValue], 2));
}] array]);
我們來看一下filtering
羔沙。為了使用ReactiveCocoa來過濾我們的數(shù)組,我們需要再一次把它序列化以便于使用過濾厨钻。
NSLog(@"%@", [[[array rac_sequence] filter:^BOOL (id value){
return [value integerValue] % 2 == 0;
}] array]);
最后看一下怎么讓一個序列流合并為單個值(folding):
NSLog(@"%@",[[[array rac_sequence] map:^id (id value){
return [value stringValue];
}] foldLeftWithStart:@"" reduce:^id (id accumulator, id value){
return [accumulator stringByAppendingString:value];
}]);
因此扼雏,我們可以非常方便地使用 RACSequence 來實現(xiàn)集合的鏈式操作,直到得到你想要的最終結果為止夯膀。除此之外诗充,使用 RACSequence 的另外一個主要好處是,RACSequence 中包含的值在默認情況下是懶計算的诱建,即只有在真正用到的時候才會被計算其障,并且只會計算一次。也就是說涂佃,如果我們只用到了一個 RACSequence 中的部分值的時候励翼,它就在不知不覺中提高了我們應用的性能。
同樣的辜荠,RACSequence 的一系列功能也是通過類簇來實現(xiàn)的汽抚,它共有九個用來實現(xiàn)不同功能的私有子類。RACSequence 為類簇提供了統(tǒng)一的對外接口伯病,對于使用它的客戶端代碼來說造烁,完全不需要知道私有子類的存在否过,很好地隱藏了實現(xiàn)細節(jié)。另外惭蟋,值得一提的是苗桂,RACSequence 實現(xiàn)了快速枚舉的協(xié)議 NSFastEnumeration ,在這個協(xié)議中只聲明了一個看上去非常抽筋的方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;
有興趣的同學告组,可以看看 RACSequence 中的相關實現(xiàn)崇猫,我們將會在后續(xù)的文章中進行介紹屠缭。因此,我們也可以直接使用 for in 來遍歷一個 RACSequence 。
- 跟
BlockKit
類似 - 看源碼感覺性能不是很好父款,有待驗證峦剔。對于幾個霹陡、幾十個的小數(shù)組皆尔,應該問題不大
訂閱者RACSubscriber
調度器RACScheduler
清潔工RACDisposable
RACCommand
ReactiveCocoa Essentials: Understanding and Using RACCommand
ReactiveCocoa對富途牛牛的一些啟發(fā)
界面控件綁定
將控件emailTextField內容賦值給self.viewModel.email
RAC(self.viewModel, email) = self.emailTextField.rac_textSignal;
將self.viewModel.statusMessage顯示到statusLabel控件
RAC(self.statusLabel, text) =RACObserve(self.viewModel, statusMessage);
統(tǒng)一處理所有異步的行為
使用信號來代表這些異步事件,提供了一種統(tǒng)一的方式來處理所有異步的行為矫俺,包括代理方法吱殉、block
回調、target-action 機制
厘托、通知考婴、KVO
等
富途牛牛項目:
- 解決KVO訂閱忘記取消訂閱的問題
- 解決訂閱通知忘記取消的問題
鏈式依賴的操作
依賴關系通常出現(xiàn)在網絡請求中,如后一個請求應該等前一個請求完成后再創(chuàng)建,等等:
[client logInWithSuccess:^{
[client loadCachedMessagesWithSuccess:^(NSArray *messages) {
[client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
NSLog(@"Fetched all messages.");
} failure:^(NSError *error) {
[self presentError:error];
}];
} failure:^(NSError *error) {
[self presentError:error];
}];
} failure:^(NSError *error) {
[self presentError:error];
}];
ReactiveCocoa 可以特別方便地處理這種邏輯模式:
[[[[client logIn]
then:^{
return [client loadCachedMessages];
}]
flattenMap:^(NSArray *messages) {
return [client fetchMessagesAfterMessage:messages.lastObject];
}]
subscribeError:^(NSError *error) {
[self presentError:error];
} completed:^{
NSLog(@"Fetched all messages.");
}];
TCP短連接可以這樣優(yōu)化
[[[[service connect] flattenMap:^RACStream *(id value) {
return [service doSomething1];
}] flattenMap:^RACStream *(id something1Value) {
// if doSomething1 is successful, 'somethingValue' is passed via sendNext
return [service disconnect];
}] subscribeError:^(NSError *error) {
// Error occurred! Handle "error" if necessary.
} completed:^{
// Asynchronous chain of operations succeeded.
}];
在異步操作上使用signals信號,讓通過鏈接和轉換這些signal信號,構建更加復雜的行為成為可能.可以在一組操作完成后,來觸發(fā)此操作即可:
// 執(zhí)行兩個網絡操作,并在它們都完成后在控制臺打印信息.
//
// +merge: 傳入一組signal信號,并返回一個新的RACSignal信號對象.這個新返回的RACSignal信號對象,傳遞所有請求的值,并在所有的請求完成時完成.即:新返回的RACSignal信號,在每個請求完成時,都會發(fā)送個消息;在所有消息完成時,除了發(fā)送消息外,還會觸發(fā)"完成"相關的block.
//
// -subscribeCompleted: signal信號完成時,將會執(zhí)行block.
[[RACSignal
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
富途牛牛項目,將很多異步操作做成獨立的RACSigal, 方便單元測試催烘,方便組織先后順序沥阱、依賴關系。比如優(yōu)化啟動流程伊群、登錄流程考杉。
網絡請求避免過于頻繁
股票網絡搜索
[[[[[[[[[self.textField.rac_textSignal
filter:^BOOL(id value) { //過濾
return [value length] > 0;
}]
throttle:0.3] //無任何輸入0.3秒后繼續(xù)
logAll]
flattenMap:^id(id value) { //flattenMap有新的搜索請求后,上一次網絡請求結果會被忽略
return [self search:value];
}]
logAll]
map:^id(NSArray *array) {
return [[[array rac_sequence] map:^id(id value) {
Stock *stock = [[Stock alloc] init];
stock.name = value[@"name"];
stock.symbol = value[@"symbol"];
stock.exch = value[@"exchDisp"];
return stock;
}] array];
}]
logAll]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id x) {
self.stocks = x;
} error:^(NSError *error) {
} completed:^{
}];
一些坑
block要記得用strongify/weakify
因為RAC很多操作都是在Block中完成的舰始,這塊最常見的問題就是在block直接把self拿來用崇棠,造成block和self的retain cycle。所以需要通過@strongify和@weakify來消除循環(huán)引用丸卷。
有些地方很容易被忽略枕稀,比如RACObserve(thing, keypath),看上去并沒有引用self谜嫉,所以在subscribeNext時就忘記了weakify/strongify萎坷。但事實上RACObserve總是會引用self,即使target不是self沐兰,所以只要有RACObserve的地方都要使用weakify/strongify哆档。
RACObserve(self, model.title) 與 RACObserve(self.model, title)
// 描述: self 本身有一個title屬性和一個model屬性,model本身也有一個title屬性.
RAC(self, title, @"") = RACObserve(self, model.title);
RAC(self, title, @"") = RACObserve(self.model, title);
這兩行代碼,有著質的不同!
RAC(self, title, @"") = RACObserve(self, model.title);
適用場景: self的model屬性改變時,動態(tài)改變self自身title屬性的值,其值為新model的title屬性.
RAC(self, title, @"") = RACObserve(self.model, title);
適用場景: self的model屬性的title屬性改變時,動態(tài)改變self自身title屬性的值,其值為原有model的title屬性.