FRP 中 EventStreams 的循环依赖

Circular dependencies of EventStreams in FRP

所有示例都使用 Ramda 作为 _(很清楚示例上下文中的方法)和 kefir 作为 frp(几乎相同 API 如 bacon.js)

我有一个描述位置变化的流。

var xDelta = frp
    .merge([
        up.map(_.multiply(1)),
        down.map(_.multiply(-1))
    ])
    .sampledBy(frp.interval(10, 0))
    .filter();

当我按下 UP 键时,它发出 +1,在 DOWN 上发出 -1

获得位置 I scan 这个增量

var x = xDelta
    .scan(_.add)
    .toProperty(0);

这是预期的工作。但我想将 x 的值从 0 限制为 1000

为了解决这个问题,我找到了两个解决方案:

  1. 改变scan

    中的函数
    var x = xDelta.scan(function (prev, next) {
        var newPosition = prev + next;
        if (newPosition < 0 && next < 0) {
            return prev;
        }
        if (newPosition > 1000 && next > 0) {
            return prev;
        }
        return newPosition;
    }, 0);
    

看起来还可以,但是以后随着新规则的出台,这个方法会越来越多。所以我的意思是它看起来不可组合和 FRPy。

  1. 我有 current 个职位。并且 delta。我想将 delta 应用到 current,前提是 current after applying 不会超出限制。

    • current 取决于 delta
    • delta 取决于 current after applying
    • current after applying 取决于 current

    所以它看起来像循环依赖。但是我使用 flatMap.

    解决了它
    var xDelta = frp
        .merge([
            up.map(_.multiply(1)),
            down.map(_.multiply(-1))
        ])
        .sampledBy(frp.interval(10, 0))
        .filter();
    
    var possibleNewPlace = xDelta
        .flatMap(function (delta) {
            return x
                .take(1)
                .map(_.add(delta));
        });
    
    var outOfLeftBoundFilter = possibleNewPlace
        .map(_.lte(0))
        .combine(xDelta.map(_.lte(0)), _.or);
    
    var outOfRightBoundFilter = possibleNewPlace
        .map(_.gte(1000))
        .combine(xDelta.map(_.gte(0)), _.or);
    
    var outOfBoundFilter = frp
        .combine([
            outOfLeftBoundFilter,
            outOfRightBoundFilter
        ], _.and);
    
    var x = xDelta
        .filterBy(outOfBoundFilter)
        .scan(_.add)
        .toProperty(0);
    

    您可以在 iofjuupasli/capture-the-sheep-frp

    查看完整的代码示例

    它正在运行演示 gh-pages

    有效,但使用循环依赖可能是反模式。

有没有更好的方法解决FRP中的循环依赖?

第二个更一般的问题

使用 Controller 可以从两个 Model 中读取一些值,并根据它的值更新它们。

所以依赖关系看起来像:

              ---> Model
Controller ---|
              ---> Model

FRP 没有 Controller。所以 Model 值应该从其他 Model 中以声明方式计算。但是,如果 Model1 从另一个相同的 Model2 计算,那么 Model2Model1 计算呢?

Model ----->
      <----- Model

例如两个具有碰撞检测的玩家:两个玩家都有 positionmovement。而第一个玩家的 movement 取决于第二个玩家的 position,反之亦然。

我在这方面还是个新手。经过多年的命令式编码,开始以声明式 FRP 风格思考并不容易。可能我遗漏了什么。

using circular dependencies is probably anti-pattern

是也不是。从你实现它时遇到的困难,你可以看出很难创建循环依赖。特别是以声明的方式。但是,如果我们想使用纯声明式风格,我们可以看到循环依赖是无效的。例如。在 Haskell 中,您可以声明 let x = x + 1 - 但它会评估为异常。

current depends on delta, delta depends on current after applying, current after applying depends on current

如果你仔细观察,它并没有。如果这是一个真正的循环依赖,current 就没有任何价值。或者 threw an exception.

相反,current 取决于其先前的状态 。这是 FRP 中众所周知的模式,stepper。摘自 this answer:

e = ((+) <$> b) <@> einput
b = stepper 0 e

在不知道 <$><@> 究竟做什么的情况下,您大概可以说出事件 e 和行为 ("property") b 是如何依赖的关于事件 einput。更好的是,我们可以声明性地扩展它们:

e = ((+) <$> bound) <@> einput
bound = (min 0) <$> (max 1000) <$> b
b = stepper 0 e

这基本上就是培根在 scan 中所做的。不幸的是,它迫使你在一个回调函数中完成所有这些。

我在任何 JS FRP 库中都没有看到 stepper 函数1。在 Bacon and Kefir 中,如果要实现此模式,您可能必须使用 Bus我很高兴被证明是错误的:-)

[1]:好吧,除了我自己实现的那个(它还不能展示)。但是使用 Stepper 仍然感觉像在跳圈,因为 JavaScript 不支持递归声明。

有一个名为 cyclejs 的新 framework/library 完全符合您描述的循环机制,但在那种情况下是类似于 Facebook 的新 React 的网络前端库。

基本思想是拥有一个模型,它是一个 "state" 值流,一个呈现这些值的视图流,一个用户交互流,它发出来自视图副作用的用户交互(浏览器 DOM) 和一个 "intent" 流,它从用户那里创建高级事件并馈送到创建新值的模型中。

它仍处于早期开发阶段,但它是一个非常巧妙的想法并且目前运行良好。