My reactive-banana library for functional reactive programming has changed considerably, though I’m not ready to release a new version just yet.
Namely, after some discussion with Conal Elliott, I have decided to
implement a subset of his classic semantics, described in his paper Push-pull functional
reactive programming. My main change to Conal’s semantics is to
remove the switcher function and to not implement
continuous behaviors.
The new semantics is a marked improvement over the previous state of my library, which did not have a well-defined semantics at all. While I think that Conal’s semantics are not the end-all – there are examples where it is less modular than one could hope for – it offers an excellent simplicity-to-power ratio. In particular, it allows for mutually recursive definitions that my previous version couldn’t handle. I think that if there’s a semantics that should serve as a reference point for every future FPR library, it’s this one.
I won’t describe Conal’s semantics here, however, that is left for a
future post or future library documentation (but see here).
Instead, I want to describe my path towards an efficient push-driven
implementation, which was far trickier than I expected. One of the main
problems was to reconcile sharing and union, that’s what I
will write about here. The other problem I’m currently wrestling with is
an implementation of accumE.
In Haskell, the first thing to do when you have a semantic model is
to implement the simplest possible incarnation of it. This is what I did
in the module Reactive.Banana.Model.
While wholly inefficient, it has the definite advantage that it is
likely correct.
Once you have an obviously correct implementation of the model, you can often derive an efficient one. Yes, that’s right, there are almost mechanical methods to obtain efficient implementations from hopelessly inefficient ones. If you don’t know about this already, I recomment to read functional pearls by Richard Bird, Graham Hutton and others. This is one of the key design methodologies for purely functional programming.
(You might think that this approach is unnecessary for simple
problems; that there should be no harm in starting right with the
efficient implementation because the problem is so simple. I have to
admit that I followed this line of thought until recently, but I have
changed my mind: there is harm in doing that. Namely, I have
found that due to a lack of training, I’m unable to
quickly find the most obviously correct implementation! Exercise: write
a program that finds the sign of a
permutation. If you start with a foldr or even
primitive recursion, you have already missed the most obviously correct
implementation.)
In my case, I haven’t been able to formally derive the push-driven implementation yet, mainly because of the issue of sharing which I will come to in a minute. Does anyone know how to derive programs with sharing?
Still, the model is very valuable, because I can now use QuickCheck to test whether my subsequent push-driven implementation is correct!
To keep the discussion simple, the basic object of consideration shall be a function
network :: Event A -> Event B -> Event C
that maps two input events to an output event. For instance, the input events might be mouse clicks and key presses while the output event some graphics that is drawn on the screen in response.
An example network is given by the following illustration, where
e1 and e2 are input events while
eout is the output event.
This corresponds to some code
eout, e1, e2, e3, e4, e5 :: Event Something
eout = union e4 e5
e4 = filter p e3
e5 = apply f e3
e3 = union e1 (filter q e2)
Imagine that the arrows are labeled with some fmap and
filter; I have not bothered giving a more concrete
example.
Now, the model implementation is inefficient because it is
demand-driven, which means that evaluation starts at the output
event and follows the arrows in reverse direction to calculate its
occurences. Of course, if one of the arrows was a successful
filter, this may have been a waste of time.
In contrast, a push-driven implementation starts at the
input events and follows the arrows until the output event is reached.
This has the advantage that evaluation aborts early if a
filter decides to throw it away. So, a push-driven
implementation would walk the following paths trought the network:
Unfortunately, this network would unnecessarily recaclulate the event
e3. We have to pay attention to sharing; a proper version
look like this:
Note that the semantics of union force us to keep the
order of paths intact; we cannot reorder paths to avoid the
crossovers.
After some stupid ideas, I finally represented events as a standard
abstract syntax tree. The main problem was to make sharing observable.
After all, since Haskell is referentially transparent, you can’t
normally distinguish between the shared variable e3 and its
“copies”. I thought that using less syntax trees would make it easier to
keep sharing, like in the model implementation, but that didn’t work out
at all.
There are several known approaching to observable sharing in a syntax
tree, but unfortunately, none of them applied straightforwardly because
the situation here involves arbitrary types. A recent
suggestion by Gill
is to use the Typeable class, but then I would have to
contrain all the combinators, like
union :: (Typeable a) => Event a -> Event a -> Event a
fmap :: (Typeable a, Typeable b) => (a -> b) -> Event a -> Event b
which is an undue burden on user type signatures and disallows
standard classes like Functor.
An older approach modeled after Claessen
and Sands is to use unsafePerformIO to assign a unique
number to every value, but there is no type-safe way to associate this
number with data. Fortunately, it is possible to replace the number with
an IORef to the associated data; that’s what I ended up
doing. The price to pay is that the abstract syntax tree is now tied to
my particular compilation scheme.
Incidentally, it would be possible to use the
StableName a that Gill employs internally to the same
effect if only they would support a generalized equality test
data Equal a b where
Equal :: Equal a a
equal :: StableName a -> StableName b -> Maybe (Equal a b)
This allows you to wrap up existentially quantified versions of
StableName into a list and extract them with the right type
again later. So, if you happen to be a GHC hacker reading this, that
would be something nice to have because I think it would allow me to
dispense with unsafePerformIO.
Anyway, after being able to observe sharing, compiling
union was fairly straightforward. Now, I’m wrestling with
accumE.