While writing an example program for my reactive-banana library, I’ve stumbled on a problem concerning simultaneous events.
Imagine that we want to implement a counter that can be decremented, but not below zero. The first code that came to my mind was the following:
edecrement :: Event ()
edecrement = ... -- some given event
-- event that indicates when the counter will be decreased
ewillDecrement :: Event ()
ewillDecrement =
filterTrue . apply ((\c () -> c > 0) <$> bcounter)
$ edecrement
where
filterTrue = fmap (const ()) . filter (== True)
-- counter that accumulates the decrements
bcounter :: Behavior Int
bcounter = accumulate (const $ subtract 1) 10 ewillDecrement
In other words, we are given event edecrement that
indicates that the counter should be decremented. From this, we
construct an event ewillDecrement that discards all illegal
decrements. Finally, the behavior bcounter accumulates the
decrements.
When I tried to run my program, it promptly got stuck in an infinite loop. At first, I thought there was a bug in my library, but as usual with Haskell, it quickly dawned on me that there was a bug in my thinking. Namely, the problem is this:
The counter cannot change simultaneously with the event.
After all, if the counter is 1 and a decrement event
arrives, will this event be checked against a value of 1
(before decrement) or 0 (after decrement)? Certainly, the
counter cannot have both values simultaneously.
One could argue that there is only one sensible solution, namely that
the counter must be checked against 1 because the other
option violates causality. But my objection to this line of reasoning is
that it argues by contradiction. You would also assign the value
1 to the program loop = 2-loop simply because
all other options are undefined, but that’s clearly not a good idea.
Syntactically, the problem is manifest in the fact that
ewillDecrement and bcounter are mutally
dependent on each other.
In a sense, the ewillDecrement event is a lie: it
actually consists of two events, one that happens before the counter is
decremented and one after it is decremented. This is especially
important for additional behaviors that depend on
ewillDecrement.
I think the most sensible semantics for the program above is indeed to be undefined. Simultaneous changes simply don’t mix.
How do other FRP approaches deal with this example?
Of course, the solution is to these kind of problems is to add some kind of “infinitesimal delay”, but I think one has to be careful with their semantics.
Ideally, one would have some kind of operator
after :: Event a -> Event a
with the property that the event after e always happens
a tad later than the event e and that this property
distributes through fmap and so on. I have a feeling that
this combinator might be impossible to implement, though. At the moment,
my library only offers an operation orderedDuplicate which
derives two new events instead of relativizing an already existing
one.
Also, on could make the implicit guarantee that
accumulate always delays the result with respect to the
input events; but I think it’s better if the programmer has to make this
explicit with an after combinator.
If you are an aspiring Haskell programmer interested in FRP and are beginning to worry whether this makes FRP infeasible: worry not, there are other ways to express this particular problem (exercise!).
And besides: this problem doesn’t go away when you switch to the
imperative style! You still have to worry whether other events that
depend on ewillDecrement should be executed before or after
the counter has been decremented. I think it’s actually a cool thing
that my program refuses to work at all if I forget to specify that.
Even a tiny bit of FRP is better than no FRP, and I believe it is beneficial to publicize a dead simple but working FRP library to get everyone on board first. We can always worry about the tricky stuff (richness of semantics, dynamic events, …) later.