当我们都用习惯 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

每个 most 的 Stream 都是一个 functor,因此我们可以 map 一个函数到流上。

  most.from([1,2,3,4]) // (ref:mostfrom)
      .map(_=>_*2)
      .observe(_=>console.log(_)); // (ref:observe)

这段代码会依次输出 =2 4 6 8=。

  • most.from 会从一个数组生成一个 most 流,跟之前的 most.fromEvent 生成一个输入流一样。
  • observe 用于观察流内的数据,每次流的数据变化,都会触发 observe 上的回调。

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前的可能,其实应该是同时的。