上一篇谈了谈我自己对函数式编程的理解。这篇文章会讲到,响应式编程,函数响应式编程这些又是个啥,以及我们为什么要使用它们。
响应式编程
对于响应式编程,我没有找到比这篇文章更为生动详尽的文章了,因此这里大部分是翻译自原文,加上了一些我自己的思考。
输入和输出
本质上来说,我们构建应用时,都是在做一件事情:等待一些事件的发生,来提供一些信息作为输入。我们根据这些输入的信息,进行某些处理,生成特定的结果并输出。
输入可以是多种多样的:「用户点击了一个按钮」是一种输入;「服务器有数据返回了」是一种输入;某个方法的回调是一种输入;或者某个对象的某个属性的变化也可以是一种输入。看着很眼熟哈,我们每天都在和这些输入打交道:
///
// 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)
,比如下面这段代码:
好,回忆一下我们是怎么去做到的呢?用什么去追踪时间轴上前面发生的故事的呢?我们也很无奈啊,我们只好引入了一个又一个的「状态」。
状态(State)
什么是「状态」?「状态」是程序运行中的参数的记录,是程序“现在长啥样”的描述。
@property (nonatomic, assign) BOOL userIsLoggedIn;
@property (nonatomic, assign) BOOL menuDataIsLoaded;
@property (nonatomic, assign) BOOL isMenuShowing;
...
这些是为了解决上面那个恼人的问题所需要记录的状态。当时间轴上有事件发生的时候,我们去更新这些状态;如果某个输出需要用到这些信息,我们再去检查这些property
当前的值。我们手动去追踪程序的状态,在各个必要的地方去更新它们,然后在一个名为xxxUpdate
的方法中,写一些复杂的判断逻辑来根据这些状态给出我们的输出:
// 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框架中也为我们提供了不少响应式编程
的支持,例如Autolayout
,KVO
等等。但是,有没有可能更进一步呢?
上一篇文章讲到,函数式编程中,可以将「数据」和「副作用」等封装成一个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」。这就是两者的区别所在。
其实我个人觉得,这种区分在实际的应用中对于我们来说并不重要,ReactiveCocoa
的Github主页也介绍自己为「Streams of values over time」。重要的是能够理解函数响应式
编程的思想,这样,在使用类似框架的时候,才能做到知其然并知其所以然。
Reference
The introduction to Reactive Programming you've been missing
网友评论