FRP 中 EventStreams 的循环依赖关系

Circular dependencies of EventStreams in FRP

本文关键字:依赖 关系 循环 EventStreams FRP      更新时间:2023-09-26

所有示例都使用 Ramda 作为_(很清楚方法在示例上下文中的作用(,kefir作为frp(与培根中的 API 几乎相同.js(

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

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

当我按下UP键并-1 DOWN时,它会发出+1

要获得位置,我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

使用玻璃钢没有Controller.因此Model值应从其他Model以声明方式计算。但是,如果Model1从另一个相同的Model2计算,那么Model2Model1计算呢?

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

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

我仍然是所有这些东西的新手。经过多年的命令式编码,开始以声明式 FRP 风格思考并不容易。可能我错过了什么。

使用循环依赖可能是反模式的

是和不是。从实现这一点时遇到的困难中,您可以看到很难创建循环依赖项。尤其是以声明性的方式。但是,如果我们想使用纯声明式风格,我们可以看到循环依赖是无效的。例如,在Haskell中,您可以声明let x = x + 1 - 但它将评估为异常。

current取决于deltadelta取决于current after applyingcurrent after applying取决于current

如果你仔细观察,它不会。如果这是一个真正的循环依赖,current从来没有任何价值。或者抛出了一个例外。

相反,current确实取决于其以前的状态。这是FRP中众所周知的模式,步进器。从这个答案中获取:

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

在不知道<$><@>究竟做什么的情况下,您可能知道事件e和行为("属性"(b如何取决于事件einput。更好的是,我们可以声明性地扩展它们:

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

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

我还没有在任何 JS FRP 库1 中看到stepper函数。在培根和开菲尔中,如果要实现此模式,则可能需要使用Bus我很高兴被证明是错的:-(

[1]:嗯,除了我因此而自己实现的那个(它还不能呈现(。但是使用Stepper仍然感觉像跳过箍,因为JavaScript不支持递归声明。

有一个名为cyclejs的新框架/库,它完全按照你描述的循环机制工作,但在这种情况下,它适用于类似于Facebook新React的Webfrontend库。

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

它仍处于早期开发阶段,但这是一个非常简洁的想法,到目前为止效果很好。