使用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)中,这将在下一章进行介绍。我们还要介绍其它的一些高阶函数,既然你已经理解这一张所讲解的这些概念了。