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:
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.
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?
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.
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 ondelta
,delta
depends oncurrent after applying
,current after applying
depends oncurrent
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.