谈谈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--T--T--F--
大蛇稚: -F--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

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

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

(更多marble diagram,见:rxmarbles.com)

如果你翻看文档,微软为Observable精心定义了上百种chainable的操作,可以应付大部分使用的场景。参见:reactivex.io/documentation/operators.html。你当然也可以定义自己的操作,来扩展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)

原文发表时间:2015-09-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Aloys的开发之路

OOAD与UML笔记

UML基础介绍 1.UML的定义 统一建模语言(UML)是一种图形化的语言,它可以帮助我们在OOAD过程中标识元素、构建模块、分析过程并可通过文档说明系统中的重...

1978
来自专栏逍遥剑客的游戏开发

PhysX学习笔记(3): 动力学(2) Actor

1872
来自专栏企鹅号快讯

程序员的花样编程,你到底行不行?

【导读】:说到 C/C++ 代码技巧,也许会有童鞋说 ,这是属于 C/C++ 程序员离职前恶搞之类的抖机灵。即便想,也不能干。别忘了有这样一句编程名言:「在编写...

2305
来自专栏大数据挖掘DT机器学习

Python NLTK自然语言处理:词干、词形与MaxMatch算法

CSDN:白马负金羁 自然语言处理是计算机科学领域与人工智能领域中的一个重要方向。自然语言工具箱(NLTK,Natural Language Toolkit)...

5105
来自专栏大数据文摘

UK DN AS NN WG UX AA:这是一条加密推送!

1404
来自专栏新智元

Jeff Dean推荐:用TPU跑Julia程序,只需不到1000行代码

Julia是一门集众家所长的编程语言。随着Julia 1.0在8月初正式发布,Julia语言已然成为机器学习编程的新宠。

1031
来自专栏牛客网

【后台开发】百度,头条,腾讯面经

半年了,从七月的迷之自信,到十月的0offer,迷茫、反思、不甘,各位战友的鼓励激励着我前进... 终于拿到了offer,感谢牛客网长期以来的陪伴,在此献上面经...

4525
来自专栏落影的专栏

OpenGL ES实践教程(九)OpenGL与视频混合

前言 前面的实践教程: OpenGL ES实践教程1-Demo01-AVPlayer OpenGL ES实践教程2-Demo02-摄像头采集数据和渲染 O...

5525
来自专栏ACM算法日常

PAT-CCCC练习:L2-001.紧急救援

作为一个城市的应急救援队伍的负责人,你有一张特殊的全国地图。在地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速...

751
来自专栏点滴积累

ANSJ中文分词使用方法

一、前言 之前做solr索引的时候就使用了ANSJ进行中文分词,用着挺好,然而当时没有写博客记录的习惯。最近又尝试了好几种JAVA下的中文分词库,个人感觉还是A...

4429

扫码关注云+社区

领取腾讯云代金券