使用RXCollections進行函數式編程(譯)
本書是講解函數響應式編程的,對吧。那么,在學會跑之前首先要學會走,我們首先學習怎樣進行函數式編程這樣我們才能有效的使用函數響應式編程‘
高階函數
函數式編程的一個核心概念是高階函數。根據維基百科,高階函數需要滿足以下兩個條件:
- 它講一個或者多個函數作為它的參數
- 它的返回值是一個函數
在OC中,我們經常使用塊(block)作為函數
在Apple提供的Foundation庫中我們不難發現高階函數的蹤影。考慮一個數字組成的普通的數組:
NSArray*array=@[@(1),@(2),@(3)];
我們也許想要遍歷數組的內容,并使用這些內容進行一些操作。“好的”, 你說,“只要寫一個for循環就可以完成”
得了吧,笨蛋才用這種方法,也太不入流了。而且,前面講過,不要使用for循環。我們可以使用NSArray的一個高階函數來實現。
這段代碼:
for(NSNumber*numberinarray) { NSLog(@"%@", number); }
和下面使用了高階函數的代碼是等價的:
[arrayenumerateObjectsUsingBlock:^(NSNumber*number,NSUIntegeridx,BOOL*stop) { NSLog(@"%@", number); }];
“可是為蝦米”,你問,“這更啰嗦了吧”(你小子是騙稿費的吧)。好吧, 是這樣,這是我們進行函數式編程的第一步。你可以看到,正如第一章提到的那樣,我們已經講怎樣完成這個任務,抽象為這個任務是什么。很快就會得到報償的,請相信我。
在實踐中,高階函數總是我們要做的事情的抽象。不幸的是,我們智能使用Foundation庫中的幾個高階函數,想使用更多的高階函數,智能求助于開放源代碼組織了
安裝RXCollections
我的朋友Rob Rix使用OC編寫了一個優秀的高階函數庫,叫做RXCollections。
首先,我們需要創建一個XCode工程。創建一個名為Playground的應用程序并選擇 “Single View Application”模板。我們將要這個工程中添加臺嗎。在本書中,使用FRP作為類名的前綴。
下一步我們需要安裝RXCollections。我們使用CocoaPods進行安裝,因為這事最簡單的方式。在運行以下命令安裝CocoaPods:
sudo gem install cocoapods
按照提示輸入密碼。CocoaPods安裝完成后,使用cd進入你新創建工程的根目錄,鍵入如下命令:
pod init
這將會在目錄下生成一個空的Podfile文件
#Uncomment this line to define a global platform for your project #platform:ios,"6.0" target"Playground"do end target"PlaygroundTests"do end
使用你最喜歡的編輯器(毫無疑問應該是vim),將platform:ios,"6.0" 解注釋,然后在Palyground目標下添加一行 pod ‘RXCollections’, ‘1.0’
platform:ios,"6.0" target"Playground"do pod'RXCollections','1.0' 6 end target"PlaygroundTests"do end
保存文件然后回到命令行兵執行如下命令:
pod install
這會安裝RXCollections并為你創建一個新的Xcode workspace文件。關閉Xcode工程,并打開Xcode workspace
打開 AppDelegate.m文件,導入如下的頭文件:
#import<RXCollections/RXCollection.h>
在application:didFinishLaunchingWithOptions: 方法中,創建前面提到的數組:
NSArray*array=@[@(1),@(2),@(3)];
準備工作完成,下面可以大干一場了。
映射( Map )
要探討的第一個高階函數是 "map",map 以一個序列(list,一般是Array, Set)作為參數,并將之轉換為相同長度的另外一個序列,映射初始序列的每個值到結果序列的一個新的值。一個影射平方數的map會產生如下結果:
map(1,2,3)=>(1,4,9)
當然,這僅僅是偽代碼,一個高階函數的返回值是另外一個函數而不是一個序列。那么在RXCollections應該如何使用map呢?
我們使用 rx_mapWithBlock: 方法完成這一任務:
NSArray *mappedArray=[array rx_mapWithBlock:^id(id each){ return @(pow([each integerValue], 2)); }];
這回完成相同的影射(平方影射)。mappedArray的值如下所示:
( 1, 4, 9 )
注意rx_mapWithBlock并不是一個真正的map,因為從技術上講他并不是一個高階函數(它不返回一個函數),這個庫隨后的提交中提到了這一點,在下一章中我們將看到map是如何在ReactiveCocoa的上下文中起作用的。
注意rx_mapWithBlock方法返回了一個新的數組,并沒有改變初始數組。從這一點來講,Foundation庫和函數式編程范式的思想是一致的,因為它的類默認是不可變的(immuable)
假設我們以命令式編程的范式解決這一問題:
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:array.count]; for (NSNumber *number in array) { [mutableArray addObject:@(pow([number integerValue], 2))]; } NSArray *mappedArray = [NSArray arrayWithArray:mutableArray];
這需要編寫更多的代碼,更不用說沒有用的局部變量mutableArray污染了這一區域
因此你可以看到當你需要講一個序列影射為另一個序列時,map是很有用處的。
過濾器(Filter)
另外一個重要的高階函數是過濾器,在ReactiveCocoa中是filter方法。過濾一個列表會返回一個新的列表,新的列表中的數據是通過過濾器測試(返回true)的原列表中的數據。讓我們通過一個例子說明一下:
NSArray *filteredArray = [array rx_filterWithBlock:^BOOL(ideach){ return ([each integerValue] % 2 == 0); }];
filteredArray現在的值是@[@(2)],如果不使用函數式編程而采用命令式編程的方式,代碼如下:
NSMutableArray*mutableArray=[NSMutableArrayarrayWithCapacity:array.count]; for(NSNumber*numberinarray){ if ([number integerValue] % 2 == 0) { [mutableArray addObject:number]; } } NSArray*filteredArray=[NSArrayarrayWithArray:mutableArray];
現在對函數式編程的優點應該有所了解了吧?相比命令式編程,函數式編程可以少寫很多代碼,具有更高的工作效率,會為我們節省大量的時間。在日常工作中我們會經常會做影射列表或者過濾結果之類的任務,使用高階函數map和filter,我們可以簡化我們的工作。
折疊(Fold)
折疊是一個很有趣的高階函數---它會聯合列表中的所有值,并返回一個單一的值。因此,我們經常將之稱之為 "combine"
最簡單的折疊器是結合一個數組中的所有值并計算它們的和:
NSNumber *sum=[array rx_foldWithBlock:^id(id memo,id each){ return @([memo integerValue] + [each integerValue]); }];
結果為 @(6),數組中的每個值依次調用,并傳入memo作為上一次塊循環返回的結果(memo的初始值為nil)
這還不是最有趣的,最有趣的是我么可以為memo賦一個初始值:
[[array rx_mapWithBlock:^id(id each){ return [each stringValue]; }] rx_foldInitialValue:@"" block:^id(id memo,id each){ return [memo stringByAppendingString:each]; }];
結果為@"123",讓我們瞧一瞧是怎么做的。首先講數字型的數組影射為字符串型的數組,然后執行折疊,傳遞一個空的字符串作為demo的初始值。
可以不使用RXCollections完成這一任務嗎?是可以的,但是比較繁瑣,還是這種方式直截了當一些。“是什么”而非“怎么解”的思維方式讓我們在編寫代碼時不需要思考怎么做的問題,更重要的是,在閱讀的時候也不需要思考怎么做的問題。
性能
在前面的章節,特別是最后一個例子,也許會讓你考慮關于性能的問題。對于很長的數組,相比命令式編程的方式,創建一個臨時的字符串代表上一次迭代的結果然后講數組的下一個值添加到該結果中會耗費更長的時間。
這就是現實,不可能將所有的好處都占著。幸運的是,計算機(甚至iPhone)的性能足夠強大,可以對其提供支撐,性能問題是可以忽略的。CPU的時間是廉價的,但是你的時間恰好比CPU的時間貴了那么一絲絲。所以就犧牲掉CPU而成全你。特別是當性能問題成為瓶頸時,我們可以回過頭來修改代碼讓其更有效率。
總結
我們已經看到怎樣在不需要可變變量(mutable variables)的情況下操作列表。因為RXCollections在其實現中可能用到了可變變量,但是我們不需要關心這些,因為框架已經把這些工作抽象化了。對于影射(mapping)、過濾(filtering)和折疊(Folding)這些任務來說是不重要的。(這并不是說你不需要了解RXCollections的代碼,而是說完成這些任務你沒有必要了解那么多)
在最后一個例子中我們可以看到,怎樣連接操作串獲取一個更復雜的結果。我們會在下一章講解更多關于操作串(chaining operations)的內容---事實上,這是使用ReactiveCocoa進行編程的一個主題。
在下一章中,我們湖討論關于map, filter和fold更多的知識。我們不經將高階函數應用到列表上,還要將它應用到流(stream)中,這將在下一章進行介紹。我們還要介紹其它的一些高階函數,既然你已經理解這一張所講解的這些概念了。