6

Monadic Reactive Programming in JavaScript

 3 years ago
source link: https://blog.oyanglul.us/javascript/reactive-programming
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Monadic Reactive Programming in JavaScript

Table of Contents

当我们都用习惯 Promise Monad 之后,我再来介绍一个跟时间相关的 Stream Monad,通过 Stream Monad,我们可以完成 Promise 或者是数组的奇淫巧计,而且符合所有 monad 的公理,于是我们叫它 Monadic Reactive Programming。

stream.gif

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]) // (mostfrom)
    .map(_=>_*2)
    .observe(_=>console.log(_)); // (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前的可能,其实应该是同时的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK