ReactiveCocoa学习笔记(三):响应式和函数响应式编程

上一篇谈了谈我自己对函数式编程的理解。这篇文章会讲到,响应式编程,函数响应式编程这些又是个啥,以及我们为什么要使用它们。

响应式编程

对于响应式编程,我没有找到比这篇文章更为生动详尽的文章了,因此这里大部分是翻译自原文,加上了一些我自己的思考。

输入和输出

本质上来说,我们构建应用时,都是在做一件事情:等待一些事件的发生,来提供一些信息作为输入。我们根据这些输入的信息,进行某些处理,生成特定的结果并输出。

输入可以是多种多样的:「用户点击了一个按钮」是一种输入;「服务器有数据返回了」是一种输入;某个方法的回调是一种输入;或者某个对象的某个属性的变化也可以是一种输入。看着很眼熟哈,我们每天都在和这些输入打交道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
///
// delegate
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
}
// block based callbacks
[TCAPI getCategoriesOnComplete:^(NSArray *objects, HTTPOperation *operation, NSError *error) {
}];
// target action
- (IBAction)buttonAction:(id)sender {
}
// timers
[NSTimer scheduledTimerWithTimeInterval:.1 target:self
selector:@selector(spinIt:) userInfo:nil repeats:YES];
// KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
}

这些都是Objective-C提供的通信机制,来给我们传达一些输入信息:某个事件发生了,怎么处理你看着办吧。(题外话:objc.io的这篇文章对于这些通信机制讲的很好)

而我们的输出也是各种各样的:我们可以将一些信息保存在本地;或者我们可以通过网络协议将一些信息保存在服务器上;当然,对于移动端app来说,我们最主要的输出,还是根据情况去更新UI界面,以便展示新的信息给用户。

线性编程(Linear Programming)

然而,麻烦的事情来了:我们的每个输出,几乎不会只和一个输入相关:当收到一个用户点击事件的输入时,我们需要更新一个UI界面,但是界面的更新往往也依赖于服务器的数据返回,或是之前用户的其他操作;更麻烦的是,我们的输入和输出是异步的:我们的输出和输入在时间顺序上是分离的。

造成这个麻烦的原因是:我们传统的编程处理输入和输出的方式是线性的(Linear Programming),比如下面这段代码:


可以看到,我们的每段代码在时间顺序上都是一段彼此独立的的时间范围。即使是异步的操作,比如各种callback,也不过是让我们在时间轴中间插入一段代码去执行。(这里没有说到多线程。然而即使是多线程,也只是在另一个线程上开辟了一条新的时间轴,开始一段新的线性编程的故事……)

现在让我们来看看上面说的麻烦事儿吧——当我们接收到某个输入事件的时候,我们往往需要作出相应的输出,这个输出往往不仅取决于当前的输入,而且是和时间轴上处于前面的输入造成的结果相关的:

哎呀用户点击这个「菜单」按钮了,这个时候应当展示出下拉菜单了。但是,首先要确认的是,用户当前是否已经登录了呢?这个菜单所需要的数据是否已经从服务器拿到了呢?用户之前点过这个按钮了么?现在是应当展示这个菜单还是收起这个菜单呢?……

相信我们对这样的逻辑也是再熟悉不过了,我们每天都在这样写代码(手动滑稽)。我们的程序像个傻瓜金鱼,只有7秒钟的记忆。这样说太夸张了,其实我们的程序连1毫秒的记忆也没有:)。我们只好在有输入到来的时候,回过头再去check一遍之前时间轴上发生的事情,检查一些必要的信息,然后做出输出。


好,回忆一下我们是怎么去做到的呢?用什么去追踪时间轴上前面发生的故事的呢?我们也很无奈啊,我们只好引入了一个又一个的「状态」

状态(State)

什么是「状态」?「状态」是程序运行中的参数的记录,是程序“现在长啥样”的描述。

1
2
3
4
@property (nonatomic, assign) BOOL userIsLoggedIn;
@property (nonatomic, assign) BOOL menuDataIsLoaded;
@property (nonatomic, assign) BOOL isMenuShowing;
...

这些是为了解决上面那个恼人的问题所需要记录的状态。当时间轴上有事件发生的时候,我们去更新这些状态;如果某个输出需要用到这些信息,我们再去检查这些property当前的值。我们手动去追踪程序的状态,在各个必要的地方去更新它们,然后在一个名为xxxUpdate的方法中,写一些复杂的判断逻辑来根据这些状态给出我们的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// a central function that checks all our states and generates the appropriate output
- (void) checkAndUpdateMenuStatus {
if (self.menuShouldBeShowing && !self.isMenuShowing
&& self.menuDataIsLoaded && self.userIsLoggedIn) {
[self showMenu];
} else if (!self.menuShouldBeShowing && self.isMenuShowing) {
[self hideMenu];
}
}
// sets initial states and sets up our notification observation
- (void) viewDidLoad {
// Set initial states
// Let's assume you can't get to this page without being logged in
self.userIsLoggedIn = YES;
self.isMenuShowing = NO;
self.menuDataIsLoaded = NO;
self.menuShouldBeShowing = NO;
// Need to handle in case the user logs out while on this page
[[NSNotificationCenter defaultCenter] addObserverForName:kUserLoggedOutNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
self.userIsLoggedIn = NO;
[self checkAndUpdateMenuStatus];
}];
// set the initial state (somewhat unnecessary since our menu starts hidden
// but a good safety check)
[self checkAndUpdateMenuStatus];
}
// Loads the menu data from the network
- (void) loadMenuData {
[TCPAPI fetchUserMenuData onComplete:^(NSArray *objects, NSError *error) {
[self hideLoadingView];
if(!error) {
self.menuDataIsLoaded = YES;
[self checkAndUpdateMenuStatus];
}
}];
}
// handles showing and hiding a loading view
- (void) startLoadingView {
if(self.isLoadingShowing) return;
self.isLoadingShowing = YES;
// do work to show loading view
}
- (void) hideLoadingView {
if(!self.isLoadingShowing) return;
self.isLoadingShowing = NO;
// do work to hide loading view
}
- (void) showMenu {
// show menu
self.isMenuShowing = YES;
}
- (void) hideMenu {
// hide menu
self.isMenuShowing = NO;
}
- (IBAction) userTappedMenuButton:(UIButton *menuButton) {
// kick off loading of our menu data lazily if it isn't loaded yet
if(!self.menuDataIsLoaded) {
[self loadMenuData];
}
self.menuShouldBeShowing = !self.menuShouldBeShowing;
[self checkAndUpdateMenuStatus];
}

(心累……原文作者你平时是在看着我写程序吗,还是说天下程序猿都是一样的傻的可爱)

上面列举的仅仅是更新一个UI所需要的状态。糟糕的是,当我们需要新的信息的时候,我们往往会不假思索地再添上一个property,毕竟已经形成了肌肉记忆了。慢慢地,我们的代码里充满了这些property,以及状态判断的if...else...逻辑。如果有一个地方出了bug,我们得慢慢去找,哪个环节让我们亲爱的状态出了问题。更可怕的是,状态带来的复杂度是随着状态数量增加呈指数级增长的——上面3种状态便能带来2^3种情况,前提还是这3种状态都是一个BOOL值……

响应式编程(Reactive Programming)

既然状态这么不好,那我们可不可以不要它了呢?聪明的猿们想到了种种方法,让计算机来帮我追踪和记录这些状态。而我们的工作是在时间轴的开始,就向计算机解释清楚:我需要哪些输入信息才能做出一个特定的输出,对于一些输入,我需要做出什么输出,剩下的事情,就交给计算机去做啦。

最常见的例子就是我们的「AutoLayout」:我们向计算机说道:“嗯,这个页面放在这个页面里,它的高度是父页面的一半,上边距为10dp,左右居中展示”。然后就哦啦。计算机会在父页面的大小和布局发生改变的时候,帮我们去调整子页面的大小和位置,而不需要我们在各个地方手动去写一堆setFrame:方法。

这就是响应式编程(Reactive Programming):我们代码里,只是说明了各个事件(输入)的关系,以及它们相应的输出。当这些事件(输入)发生的时候,计算机根据我们的说明,去进行恰当的响应。「状态」依然是存在的,只不过我们将它们托付给了计算机去处理。响应式编程处理了时间轴上输入和输出的异步问题,让我们轻装上阵,对付各种各样的业务逻辑。

移动app时代,随着UI元素越来越多,用户交互越来越复杂,处理越来越频繁,需要的实时性也越来越高,这也是响应式编程越来越受到开发者们的青睐的原因吧。

函数响应式编程

响应式编程给我们带来了许多的好处,Cocoa框架中也为我们提供了不少响应式编程的支持,例如AutolayoutKVO等等。但是,有没有可能更进一步呢?

上一篇文章讲到,函数式编程中,可以将「数据」和「副作用」等封装成一个monad,然后就可以尽享函数式编程的链式编程的丝滑体验了。那如果将我们响应式编程中的「输入」和处理它们的「异步」的逻辑,抽象成一个monad呢?那么,我们将可以使用链式语法和各种强大的函数式编程的工具,处理各种「输入」,以及让「输入」在函数式的“管道”中经过一步步地处理,最终成为我们需要的「输出」。

没错,这就是函数响应式编程的魅力了!它将一个随时间变化的值抽象成一个流,并通过monad使其可以利用到函数式编程的强大工具,最终让我们可以方便直观地处理各种「输入」和「输出」的异步处理逻辑。

Functional reactive programming (FRP) is a programming paradigm for reactive programming (asynchronous dataflow programming) using the building blocks of functional programming (e.g. map, reduce, filter). – Wikipedia

函数响应式编程&函数式响应式编程

网络上的教程都说,ReactiveCocoa(以及RXSwift)是一个函数式响应式的编程框架,而没有说是“函数响应式框架”,让人傻傻分不清楚。这是为什么呢?

在上文中,其实强调了函数响应式中抽象出的值流是随时间连续变化的,其抽象称为「behaviors」;而像ReactiveCocoa(或是RXSwift)这类框架是应用于主要处理人机交互的移动软件的,它抽象出的「输入」流是时间轴上离散的一个个事件,称为「Event」。这就是两者的区别所在。

其实我个人觉得,这种区分在实际的应用中对于我们来说并不重要,ReactiveCocoaGithub主页也介绍自己为「Streams of values over time」。重要的是能够理解函数响应式编程的思想,这样,在使用类似框架的时候,才能做到知其然并知其所以然。


Reference

Why Reactive(Cocoa)?

The introduction to Reactive Programming you’ve been missing

stackoverflow - What is functional reactive programming