iOS的函数响应式编程(三)

849 阅读9分钟

本书译者为: kevinHM

翻译自 leanpub.com/iosfrp

用RXCollections进行函数式编程

这本书的内容是关于函数响应式编程的,但我们不能还不会走路就想要跑起来,所以学会怎样入手函数式编程后才能更有效率的进行函数响应式编程。

使用高阶函数进行函数式编程

“高阶函数”是函数式编程中的关键的知识。从维基百科的解释来看,一个高阶函数应该达到以下的两个要求:

  • 一个或者多个函数作为输入。
  • 有且仅有一个函数输出。

在Objective-c中我们经常使用block作为函数。我们不需要跋山涉水地去寻找“高阶函数”,实际上,Apple为我们提供的Foundation库中就有。考虑象下面这么简单的一个NSNumber 的数组:

 NSArray * array = @[ @(1), @(2), @(3) ];

我们想要枚举这个数组的内容,利用数组元素来做些事情。

我们可以用一个NSArray的高阶函数来代替for循环。代码如下:

for (NSNumber *number in array) NSLog(@"%@",number);

。。。这个等同于下面的高阶函数:

[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop)
{
    NSLog(@"%@",number);
}];

"为什么?","这代码不是更多了吗?".

好吧,确实是这样,但这是通往函数式编程道路上的第一步:函数的启蒙教育。就像上一章节所说的,如何在只关注任务本身的前提下去完成任务?这只是为即将到来的便利付出的一点点代价,相信我。

实际上,高阶函数是很抽象的东西,我们所做的事情(命令式编程)基本上都可以用它来抽象。但Foundation中高阶函数的程度很低,要了解更多,我们不得不借助开源社区。

如何使用RXCollections?

我的朋友RobRix使用OC写了一个称为RXCollections的出色的高阶函数的库(译者注:目前这个项目作者已经停止维护,取而代之是RobRix的另外一个项目Reducers)

首先,我们需要一个可以展示的Xcode工程,创建一个新工程“Playground”。选择"Single View Application"作为模板。我们将在AppDelegate中展示绝大部分代码。在本书中,我将使用"FRP"作为类的前缀。

其次,我们需要在工程中导入RXCollections.我将使用Cocoapods导入这个库,这会让事情变得简单。使用如下命令以确保你的电脑里安装了最新的cocoapods。

sudo gem install cocoapods

终端出现提示的时候输入你的root密码。一旦cocoapods已经安装好了,使用cd导航到刚刚新建的工程目录下,并在终端输入如下指令:

pod init

这将会在当前目录下为你生成一个新的文件Podfile.内容大致如下:

#Uncomment this line to define a global platform for your project
#platform :ios, "6.0" (这里为m.n 取决于工程的设置)

target "Playground" do

end

target "PlaygroundTests" do

end

用你最习惯的文本编译器(我猜是Vim),取消#platform :ios,"6.0"的注释标示,并添加 pod 'RXCollections' , '~> 1.0'到"Playground"下。

platform :ios, "6.0"

target "Playground" do

pod 'RXCollections', '~> 1.0'

end

target "PlaygroundTests" do

end

好了!保存并退出编辑器,回到终端并输入:

pod install

这将导入RXCollections到工程中,同时为你提供一个新的xcode workspace文件。关闭当前xcode工程,用刚刚生成的workspace文件打开工程。

在Appdelegate.m文件中引入如下头文件:

#import <RXCollections/RXCollection.h>

在application:didFinishLaunchingWithOptions:方法中,创建一个我们之前讲到的数组。

NSArray * array = @[ @1, @2 , @3 ];

好了,学会了使用RXCollections,我们就开始进行函数式编程吧!

学习高阶映射

高阶映射是学习高阶函数的第一个内容。映射能够把函数中已经存在的一个列表变成另外一个具有相同长度的列表,并且新的列表中的每个值都是与原始列表一一对应的。如下所示是一个平方数的映射:

map(1,2,3) => (1,4,9)

当然,这只是一个伪代码,一个高阶函数会返回另外一个函数而不是一个列表。那么我们要如何利用RXCollections呢?

我们这么来用rx_mapWithBlock:方法:

NSArray * mappedArray = [array rx_mapWithBlock:^id(id each){
    return @(pow([each integerValue],2));
}];

这将会达成上面伪代码所完成的任务,如果我们打印出array的日志,我们将会看到如下内容:

(
    1,
    4,
    9
)

简直完美!请注意rx_mapWithBlock: 并不是一个真正的函数映射,因为他不是技术上的高阶函数(她没有返回一个函数)。后面提到的库(RAC)已经解决了这一点,在下一章我们将看到映射是如何在ReactiveCocoa的上下文中工作的。

注意rx_mapWithBlock:在没有对原数组元素进行任何修改的前提下返回了一个新的数组,这里Foundation的类真的是非常好用的一个例子,因为他们的类默认就是不可变的。

想象一下,往常(命令式编程)为了完成这个任务,我们不得不写下这样的代码:

NSMutableArray *mutableArray = [NSMutableArray arryaWithCapacity:array.count];
for (NSNumber *number in array) [mutableArray addObject:@(pow([number integerValue], 2))];

NSArray *mappedArray = [NSArray arrayWithArray: mutableArray];

代码显然更多,而且还有一个无用的局部变量mutableArray污染了我们的作用域,简直浪费!

所以当你想把一个列表里的元素转化为另一个列表的元素时,你就能体会到高阶映射的强大。

高阶过滤的简单使用

过滤器是与ReactiveCocoa相关的另一个关键的高阶函数,我们接下来来学习如何使用高阶过滤。一个列表通过过滤能够返回一个只包含了原列表中符合条件的元素的新列表,接下来我们通过具体的例子进行实践:

NSArray *filteredArray = [array rx_filterWithBlock:^BOOL(id each){
    return ([each integerValue] % 2 == 0);
}]

过滤后,现在filteredArray等于@[ @2 ].如果没有这样的抽象方法(即高阶过滤),我们不得不像下面这样来完成工作:

NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity: array.count];
for ( NSNumber * number in array ){
    if ( [number integerValue] % 2 == 0 ){
        [mutableArray addObject:number];
    }
}
NSArray *filteredArray = [NSArray arrayWithArray:mutableArray];

我们在每天的工作中可能会涉及到类似这种高阶映射或者高阶过滤的事情多不胜数。这样通过使用类似高阶过滤、高阶映射的高阶函数,我们能够把繁琐又乏味的任务抽象出来,让工作变得更加轻松。

高阶折叠如何使用?

Flod 是一个比较有意思的高阶函数,它能够把列表中的所有元素变成一个值。通常我们会使用一个简单的高阶折叠来对数值数组进行求和操作。

NSNumber * sum = [array rx_foldWithBlock:^ id (id memo , id each){
    return @([memo integerValue] + [each integerValue]);
}];

输出的值为@6.数组中的每一个元素按顺序执行上述合并规则:[memointegerValue]+[eachintegerValue],其中memo参数纪录的是上一次合并后的结果,其初始值为零。这还不是很有趣,有趣的是我们还能给memo(这个参数的泛称)赋初始值:

[[array rx_mapWithBlock:^id (id each){
        return [each stringValue];
    }] rx_foldInitialValue:@"" block:^id (id memo , id each){
        return [memo stringByAppendingString:each];
}];

代码的结果:@“123”. 我们来分析一下这是怎么做到的. 首先我们对数组中的所有NSNumber对象做了映射,把他们变成了NSString对象,然后我们实现了一个高阶折叠,并给了memo变量一个空字符串。

在没有RXCollections的情况下能得到这样的结果吗?当然可以。但这是一个明确的"是什么,而不是如何"的解决问题的方法。这种方法可以让我们不必跟CPU一样去想"这一步要如何,下一步要如何"类似这样的事情。写代码的时候如此,读代码的时候更是如此(意:更多地关注任务是什么,要达成什么目标)。

函数式编程性能损耗大吗?

本章中有关于函数式编程的一些代码实例会让我们开始关注性能方面的影响。比如,在一个长数组中,如果给每个元素创建一个过渡的字符描述并把他们追加到前面的结果中去,这与命令式的编程对比,消耗的时间会更多。

这个问题或许会困扰我们,但我们知道现在的计算机(甚至iPhone手机)性能已经足够强大,在大多数情况下,这种性能损耗其实是可以忽略的,况且当这种损耗达到一个性能瓶颈的时候,你随时都可以重新去优化它以让它变得更加高效。相比于我们的时间,CPU的时间则显得很廉价,因此牺牲CPU的时间会是更好的选择。

总结用RXCollections进行的函数式编程

在过去的章节中,我们使用RXCollections后不需要额外的可变变量就可以在列表上进行操作,虽然RXCollections可能隐式地生成了这样的可变变量来完成任务,但是这不是我们要关心的,因为它已经为我们抽象出了这样的方式,通过:mapping\filtering和folding这种方式让我们不必在意实现任务的步骤。(当然,这并不是说,我们不应该熟悉RXCollections的源码,只是告诉你不必按部就班地去完成任务了)

在最后的章节中,我们也看到了,使用链式操作一次可以输出一个更为复杂的逻辑操作的结果。下一章我们将谈论更多的有关链式操作的内容———实际上,它是ReactiveCocoa中的重要语法之一。

下一章,我们将要讨论更多的有关映射、过滤及折叠相关的内容。我们不仅仅局限于将高阶函数运用在列表的操作上,我们也将用她们来操作流(译者注:一切皆文件,文件以流的形式传播,也就是说一切的操作都可以使用高阶函数),还会介绍其他的高阶函数。