谈谈FRP和Observable(一)

谈谈FRP和Observable(一)

陈天陈天

Observable是方兴未艾的FRP(Functional Reactive Programming)革命里最引人注目的一把火炬。FRP发展了也有两年多了,至今为止,还没有一个很好的定义,wikipedia上的定义和reactivemanifesto.org上的说辞要么太抽象,要么太泛泛。我比较喜欢如下这样一个定义:


FRP is about "datatypes that represent a value 'over time'"

因为它点出了最关键的要素:时间。在FRP出现之前,几乎没有一种软件思想认真考虑过时间这个纬度,即便考虑,也是把时间单独处理,就像爱因斯坦之前的物理学割裂时空一样。在旧有的观念里,变量随着时间的流逝,因着事件的触发虽然不断变化,但它依旧是时空轴上的一个点(一维),而非一条线(二维)。


Elm(一门脱胎于haskell的compile-to-javascript的FRP语言)和ReactiveExtensions(微软对FRP的总结)尝试着改变这一认知。Elm提出了Signal的概念,很形象,可以理解为一个和时间相关的序列。



你可以在Signal上做任何的computation(map/reduce/fold/merge/…),但要保证输出依旧是一个Signal。





有了Signal的概念,变量不再是一个个一维的,离散的数据,而是随着时间一路延展下去的一个流(stream)。此外,函数式编程让人伤神的immutable特性在Signal的概念下很好地和我们熟知的程序世界统一起来:在这个流里,每个单个的值在产生的那一刻就固定下来(immutable),但整个流是不断变化的(是不是有种电磁学和光学统一的既视感?)。一个变量,其状态在t0是a,t1是b,t2是c,那么用Signal表述就是 [a, b, c, …]。


在这种时空观下,原有的概念可以被很好地囊括进去,一如牛顿的经典力学是相对论力学的一个子集一样。比如,一个值为x常量,可以被视作随着时间变化的一个恒定的数据流,用Signal表述就是 [x, x, …]。


有了这样一个概念,我们可以以一个全新的角度去考虑代码。驱动程序运行的最原始的Signals成为 "single source of truth",我们需要做的就是对其map,filter,merge,groupBy,…等各种个样的composable transformation,派生出来一个个新的Signals,最终在输出的时候根据需要reduce。


Composable transformation一直是程序员苦苦追求的一个境界,而在FRP的世界里,它成为了一种随处可见的标配(哭)。我们稍后再给出一些composable transformation的例子。


由此,很多原本难以处理的事情可以被清晰地概念化,从而被很直观地处理。如果我们把鼠标单击的事件看成一个Signal,那么双击是在这个Signal上filter出来的,200ms(假设双击的阈值是200ms)内发生两次单击的Signal。



同理,kof97里面草薙的绝招大蛇稚 "下 后 下 前 拳",是keyup Signal在一定时间阈值内filter出来值依次是"下 后 下 前 拳"的Signal,这个Signal再和一组在某个时间点上草薙是否有足够的气发绝招的无限序列组[False, False, False, True, True …]组成的Signal一merge,再map一下,就是一个是否发绝招的Signal。


keyup: ---r--下---上----下-下-后--下--前--拳-拳---
buffer:--------r下上------------下下后下前拳拳----
气够否: ---F---F---F---T---T---T---T---T---F---
大蛇稚: ---F---F---F---F---F---F---F---F---T---

当然,本文的主角不是Elm,所以让我们跳过Elm,来讲讲概念上相同,实现上有些差异的Observable,它是ReactiveExtension里面最重要的一个概念。Elm和ReactiveExtensions最大的不同是,前者是一门语言,后者是与语言无关的一组概念和思想,以及这个思想在各个已知语言的实现。对Elm感兴趣的读者可以访问:elm-lang.org获取更多细节,以及看Evan Czaplicki在StrangeLoop上的精彩演讲:Taxonomy of FRP: controlling time and space(youtube,自备tizi)。


Observable从名字上看大概可以猜到是从Observer pattern演化而来的。典型的observer pattern在运行时是这样一个时序:



整个过程是同步完成的。而Observable将这个概念延伸到了异步处理当中。和Elm的Signal很像,Observable也是一个随着时间不断延展的数据流,只不过,这个数据流除了产生数据之外,还可以产生可选的错误信号和终止的信号:



任何第三方可以subscribe这个Observable,获取其数据。先不说废话,我们看一个Observable的例子(RxJs):



和上次文章里讲到的Promise类似,要创建一个Observable你需要提供一个参数为 observer 的回调函数。在这个回调函数里,你可以生成三种事件:


  • onNext:产生下一个数据

  • onError:产生错误信号。注意一旦onError发生,Observable随后会调用你提供的dispose方法,来清理回收相关的资源(如果需要的话)

  • onCompleted:产生结束信号。当这个信号发生后,Observable的生命周期结束,dispose方法会被调用进行清理回收。

在你的回调函数结束之前,你可以返回一个函数(可选),这个函数会在Observable进行 dispose 的时候被调用。


嗯,一个Obervable的定义就这么简单,和Promise相比,并没有复杂多少。


在使用方面,Observable是lazy的。cold Observable只有在 subscribe 的那一刻才被调用,hot Observable只有在 connect 发生的那一刻才开始服务。



(要访问这段代码,请移步:jsbin.com/duqaya/5/edit


至于什么是cold Observable,什么是hot Observable,聪明如你看了代码也猜了个八九不离十:一个Observable一旦被 publish 出去,便成了hot Observable,从 connect 的时刻起,不管有没有人 subscribe,就一直在生成下一个数据,直至 onError 或者 onCompleted 为止。在不同时间节点连接上来的subscriber,会获得那个时间节点起所有的数据。嗯,典型的 Pub/Sub


在上面的例子里我们还注意到两个新的函数:interval 和 map。这是Observable真正强大的地方:它不仅提供了一种思想核心(value over time),还提供了围绕着这个核心的生态圈:让人眼花缭乱的各式操作。


interval必多说,在间隔的时间(500ms)内,吐出[0, 1, …]这样一个序列;map用marble diagram表述,是这样一个概念:



(更多marble diagram,见:rxmarbles.com


如果你翻看文档,微软为Observable精心定义了上百种chainable的操作,可以应付大部分使用的场景。参见:reactivex.io/documentat。你当然也可以定义自己的操作,来扩展Observable的能力。我们都知道Wirth教授那著名的 "程序=算法+数据结构",如今,数据结构(Observable)和算法(operations)都给我们了,那我们能干点啥?


我们以Observable一个经典的例子来结束本文:



(访问代码请移步:jsbin.com/leroru/edit


稍稍解释一下代码:


  • 为了便于标注Dom element,我使用了jQuery经典的$前缀;为了便于标注Observable,我使用了$后缀,你不必如此撰写代码

  • R.pipe 是ramda.js的一个函数,如果经常做函数式编程的同学应该知道,它生成一个依次执行传递进来的函数的函数。在这个例子里,生成了一个函数,创建一个li节点,然后将其append到dom里。

  • throttleInput$这个Observable是这样一个序列:


    • 首先生成一个search input下的keyup的数据流 [a, b, c, d, delete, d, e, …]

    • 然后将其pluck成输入框里的文字 [a, ab, abc, abcd, abc, abcd, abcde, …]

    • 然后filter出长度大于2的文字 [abc, abcd, abc, abcd, abcde, …]

    • 然后在一个时间间隔内仅仅emit一个数据 [abc, abc, abcde, …],这是一个backpressure的机制(见下图debounce)

    • 然后仅仅返回不同的值(删了d,又按下d)[abc, abcde, …](见下图distinct)

  • suggestion$在throttleInput$基础上做了个 flatMapLatest(searchWiki),讲 [abc, abcde, …] 转换成 [abc在wiki搜索的结果,abcde在wiki搜索的结果, …]

  • searchWiki 里的 Rx.DOM.jsonpRequest(url) 也是个Observable,所以你可以用其operator: retry(见下图retry)。

几个marble diagram:




是不是很神奇?这四十多行清晰易懂,各种race condition都被消弭于无形的代码,在jQuery里,据说需要九百多行代码才能完成。你愿意写哪种代码呢?


注意,Observable是一种思想,而非一种实现,以上是RxJs的实现,我仅仅将其应用在前端而已。实际上在java/clojure/C#等代码中,都可以以相同的方式使用Observable,当然,你也可以将RxJs应用在node程序中。这是个 一次学习,到处受益 的思想。嗯,先写这么多,下次我们再讲讲如何用Observable的思想来考虑问题。


如果您觉得这篇文章不错,请点赞。多谢!


欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。

文章被以下专栏收录
15 条评论
推荐阅读