Monadic Reactive Programming in JavaScript
当我们都用习惯 Promise Monad 之后,我再来介绍一个跟时间相关的 Stream Monad,通过 Stream Monad,我们可以完成 Promise 或者是数组的奇淫巧计,而且符合所有 monad 的公理,于是我们叫它 Monadic Reactive Programming。
Stream
如果说数组是空间维度,那么 Stream 则是时间维度版本的数组。比如我们有一个数组,需要 reduce 一下再打印出结果,是非常容易做到的:
[1,2,3,4].reduce((acc,x)=>acc+x)
那么问题是,我们操作在一个空间上已经存在的数组,是非常容易的,但是如果我们的输入是随着时间变化的,该如何处理?
而在前端世界,这种情况非常常见,比如一个简单的用户输入框 input,同样的,我想得到输的总和,似乎是有些棘手的一件事情,只是,对于函数式编程来说,对于状态的保存就非常头疼。当然如果不考虑函数式,弄一个全局变量来保存 acc 也是最直接的思路了。
var acc = 0;
$('input').onChange(_=>acc+=_)
这样每次在 input 中修改数字,都会加入到 acc 中。
而不可变的函数式应该如何解决这种问题呢?
下面开始用 cujojs/most :
most.fromEvent('input', document.querySelector('input'))
.reduce((acc,x)=>acc+x)
.then(_=>console.log(_))
而这样的一组随时间变化的输入,就变成了输入流,使用 reactive programming 的技巧,我们可以像操作空间上的数组一样操作流, 从而可以使用上我们对待数组一样的奇淫巧计,这就是 reactive programming,另外如果还符合 monad 的一些公理,就会变成 monadic reactive programming。
Functor
Applicative
不仅如此,Stream 还是 Applicative Functor,希望之前的概念还记得,Applicative 可以把含有函数的容器应用到另一个含有值的容器上,所以上例可以用 Applicative 这样做:
most.of(_=>_*2)
.ap(most.from([1,2,3,4]))
.observe(_=>console.log(_))
除了使用 Applicative 之外,我们还可以把函数 lift 起来,这样在使用上跟一般的函数就没有什么区别了,只是现在 lifted 的函数可以操作 most 流。(虽然不知道为什么官网并没有推荐(deprecated) 使用 lift,反倒我觉得是用 lift 更适合函数的重用。)
var multiple2 = function(x){return x*2};
var lifedMultiple2 = most.lift(multiple2);
lifedMultiple2(most.of(3))
.observe(_=>console.log(_))
Monad
当然,most 的 Stream 同时也是 Monad,因此可以方便的 flatmap 一个返回 stream 的函数。
most.from([1, 2])
.flatMap(x=>most.periodic(x * 1000).take(5).constant(x))
.observe(_=>console.log(_));
思考一下这里如果是一个数组 [1,2]
,比如 flatMap x=>[x*2]
会得到一个展开的数组 [2,4]
,而不是 [[2],[4]]
。 同样的,flatMap 一个流,得到应该是 flat 过的流,那么这里产生的两个流, 1-1-1-1-1
,和 2---2---2---2---2
,想象一下要把两个流展开放到一个流里,空间的元素放到数组中是可以按空间排列,那么元素放到流中则是应该按照时间排列,我们做一个简单的对齐:
1-1-1-1-1 2- -2- -2--2--2 1 1 1 2-1-2-1-2--2--2
其中每一个 -
代表一秒,所以输出会是 12-1-12-1-12--2--2
。数字之间没有 -
代表会同时打印,因此有可能会出现 2 在 1前的可能,其实应该是同时的。