Search code examples
javascriptcircular-dependencyfrpbacon.jsramda.js

Circular dependencies of EventStreams in FRP


All examples uses Ramda as _ (it's clear what methods do in examples contexts) and kefir as frp (almost same API as in bacon.js)

I have a stream, that describes change of position.

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

It emits +1 when I press UP key, and -1 on DOWN.

To get position I scan this delta

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

That's work as expected. But I want to limit value of x from 0 to 1000.

To solve this problem I found two solution:

  1. Change function in 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);
    

It looks Ok, but later, as new rules will be introduced, this method will grow. So I mean it doesn't look composable and FRPy.

  1. I have current position. And delta. I want to apply delta to current, only if current after applying will not be out of limits.

    • current depends on delta
    • delta depends on current after applying
    • current after applying depends on current

    So it looks like circular dependency. But I solved it using 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);
    

    You can see full code example at iofjuupasli/capture-the-sheep-frp

    And it's working demo gh-pages

    It works, but using circular dependencies is probably anti-pattern.

Is there a better way to solve circular dependency in FRP?

The second more general question

With Controller it's possible to read some values from two Model and depending on it's values update both of them.

So dependencies looks like:

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

With FRP there is no Controller. So Model value should be declaratively calculated from other Model. But what if Model1 calculating from another Model2 which is the same, so Model2 calculates from Model1?

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

For example two players with collision detection: both players have position and movement. And movement of first player depends on position of second, and vice versa.

I'm still newbie in all this stuff. It's not easy to start think in declarative FRP style after years of imperative coding. Probably I'm missing something.


Solution

  • using circular dependencies is probably anti-pattern

    Yes and no. From the difficulties you had with implementing this, you can see that it's hard to create a circular dependency. Especially in a declarative way. However, if we want to use pure declarative style, we can see that circular dependencies are invalid. E.g. in Haskell you can declare let x = x + 1 - but it will evaluate to an exception.

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

    If you look closely, it doesn't. If this were a true circular dependency, current never had any value. Or threw an exception.

    Instead, current does depend on its previous state. This is a well-known pattern in FRP, the stepper. Taking from this answer:

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

    Without knowing what <$> and <@> exactly do, you can probably tell how the events e and the behaviour ("property") b depend on the events einput. And much better, we can declaratively extend them:

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

    This is basically what Bacon does in scan. Unfortunately it forces you to do all of this in a single callback function.

    I haven't seen a stepper function in any JS FRP library1. In Bacon and Kefir, you'll probably have to use a Bus if you want to implement this pattern. I'd be happy to be proven wrong :-)

    [1]: Well, except in the one I have implemented myself because of this (it's not presentable yet). But using Stepper still felt like jumping through hoops, as JavaScript doesn't support recursive declarations.