In this post, I am going to explain the API for dynamic event switching in my reactive-banana library, version 0.7.*.
In particular, I’m going to explain the new Moment
type. Moreover, we’ll elucidate the connection to other approaches to dynamic event switching in Haskell, like the one taken by the sodium library.
The concept of “dynamic event switching” also goes by the name of “higher-order events”. The idea is that you can make an event that contains behaviors
example :: Event (Behavior A)
and use it to define a new behavior which acts like the behavior which has occurred last in the event. This is done with the switchB
combinator, whose type and semantics ought to be
switchB :: Behavior a -> Event (Behavior a) -> Behavior a
switchB b es = \time ->
last $ b : [b2 | (time2,b2) <- es, time2 < time]
In other words, you “switch” to a new behavior whenever an event occurrence happens.
Unfortunately, for various fundamental reasons, the types above will have to be more sophisticated than indicated in the dummy examples above. The purpose of this post is to introduce and explain these more sophisticated types.
Whenever you have some dynamic collection of events or behaviors, you likely want dynamic event switching.
For instance, imagine that you are programming GUI and want to allocate new widgets dynamically. This is what you need dynamic event switching for. The BarTab.hs example gives a demonstration. Essentially, you have a dynamic collection of text entry widgets which is used to input prices. The total price is calculated from these via the switchB
combinator.
eentries :: Event [Behavior Price]
eentries = accumE [] $
(add <$> eClickAdd) `union` (remove <$> eClickRemove)
where
add x xs = x:xs
remove xs = drop 1 xs
etotal :: Event (Behavior Price)
etotal = (fmap sum . sequenceA) <$> eentries
btotal :: Behavior Price
btotal = switchB (pure 0) etotal
Note, however, that there are many cases that look like they require dynamic event switching, but which actually don’t. One instance is the Asteroids.hs example, which features a space ship and a collection of moving asteroids. You can model the asteroids as list of time-varying objects [Behavior Asteroid]
, but it is equally possible to model them as a time-varying list of objects, Behavior [Asteroid]
. The latter approach is entirely first-order, but can be less compositional than the former approach.
Now, on to the explanation of how dynamic event switching is done in reactive-banana.
The first key concept to understand is the notion of start time.
In reactive-banana, each event and behavior has a specific start time, which is indicated by the type parameter t
Event t a -- event starting at time t
Behavior t a -- behavior starting at time t
Now you know what this pesky type parameter t
is used for. Of course, you will never plug a concrete time, like 5 o'clock
, into the types. Rather, the type parameter is just a dummy that enforces some typing discipline, but I strongly recommend to think of it as an actual clock time.
The second key concept to understand is a new abstract type, namely the Moment
type (constructor). Is is on par with the Event
and Behavior
abstractions, so we now have a “reactive trinity” of types instead of the old “reactive duopoly”.
Conceptually, the Moment
type is very simple, to the point of being almost trivial. Namely, an expression
x :: Moment T A -- value of type A at time T
simply denotes a value of type A
at the specific time T
. In other words, it just tags an ordinary value with a specific time; the intention being that the tagged value is only “valid” at this particular moment in time. You also want to perform calculation with tagged values, that’s why Moment t
is a monad.
Being able to tag values with clock times is nice and well, but the real magic only starts to happen when you quantify over the time parameter. Namely, an expression
y :: forall t. Moment t A
denotes a value of type A
for each time t
. Most importantly, this value can be different for different times t
. In other words, this quantified version is extremely similar to a Behavior
!
(For those who wonder about parametric polymorphism: best think of this as a dependent product, the parameter t
is definitely not parametric, at least in spirit.)
The easiest use case for this quantified type is the observeE
combinator
observeE :: Event t (forall s. Moment s a) -> Event t a
Whenever the argument event occurs, it just unwraps the tag by setting s
to the time of occurrence.
With the Moment
monad, it now becomes possible to express the notion of an Event
or a Behavior
with a variable start time. Namely, consider the type
e :: forall t. Moment t (Event t A)
Thanks to the Moment
monad and the quantitifcation, this expression denotes an Event
with start time t
for each time t
. Likewise, the expression
b :: forall t. Moment t (Behavior t A)
denotes a Behavior
with variable start time.
The point of all this is that in reactive-banana, we may only switch between Events and Behaviors that have variable start times. In particular, the switching combinators have the types
switchE :: Event t (forall s. Moment s (Event s a)) -> Event t a
switchB :: Behavior t a
-> Event t (forall s. Moment s (Behavior s a))
-> Behavior t a
For instance, switchB
works like this: whenever the argument event occurs, its payload is a Behavior
that starts at the time of occurrence s
. This Behavior
will be switched into the result until the next event occurs. In other words, the switchB
combinator works exactly as described in the introduction, except that the types are slightly more elaborate.
Now you know how to use events and behaviors with variable start times, but I still have to tell you how to construct them. They are created by trimming existing events and behaviors. The relevant combinators are
trimE :: Event t a
-> Moment t (forall s. Moment s (Event s a))
trimB :: Behavior t a
-> Moment t (forall s. Moment s (Behavior s a))
Essentially, the trimE
function just discards all event occurrences that happen before or on the new start time s
. Similarly, the trimB
combinator discards the history of the behavior that happens before the new start time s
.
All these types are chosen such that you always have to trim an event or behavior before you can switch it in. This is necessary to ensure efficiency, i.e. the absence of time leaks, via the type system.
One last thing: if you look at the API documenation, you will notice that the types are slightly different from what I just wrote. But fret not, this is just a harmless technical restriction having to do with impredicative polymorphism. (This doesn’t sound harmless at all, right? But be assured, it is as harmless as a baby banana.)
The thing is just that GHC currently can’t deal very well with forall
inside type arguments, so I have made a new type, AnyMoment
, to appease the gaelic Haskell gods. Read it like this:
AnyMoment Event a = forall s. Moment s (Event s a)
AnyMoment Behavior a = forall s. Moment s (Behavior s a)
AnyMoment Identity a = forall s. Moment s a
The now
and anyMoment
functions provide the corresponding mappings (isomorphisms).
I think it is very instructive to put reactive-banana’s approach to dynamic event switching into context and to clarify its connection to other approaches.
I learned about the importance of trimming from a blog post by Conal Elliott, where he writes that
Failing to trim results in behavior that is incorrect and grossly inefficient. […] (the dreaded “space-time” leak)
The key point of the approach presented here is to enforce trimming via the type system, thereby avoding time leaks statically.
The idea of using a type parameter to denote start times and to control trimming goes back to a paper by Wolfgang Jeltsch. It follows Conal’s method in that trimming is tied to the occurrences of an existing event.
trim :: Behavior b -> Event a -> Event (a, Behavior b)
The key novelty in our approach is the Moment
monad, which allows us to obtain trimmed behaviors and events as first-class objects, which is a lot more flexible. Of course, the results of trimB
and trimE
are wrapped in a Moment t
monad, so you do have to specify when trimming should start, but it is no longer necessary to specify when trimming should happen.
Another first-class approach to trimming was introduced by Gergely Patai. It has also been implemented by Stephen Blackheath in his sodium library. They don’t use a type parameter to specify start times, so trimming has to be done “whenever it changes meaning”, which is whenever we accumulate state. For instance, the accumE
combinator has the type
accumE :: a -> Event (a -> a) -> Reactive (Event a)
where Reactive
is a monad similar to the forall t. Moment t
monad. In other words, events with variable start time are first-class and represented by the type Reactive (Event a)
. Compared to this method, the advantage of our approach is that the accumulation combinators can retain their simple types as we have separate control over trimming via the trimE
and trimB
combinators. Of course, only time will tell whether this is actually an advantage in practice.
Apart from utility of the Moment
type as a device for enforcing typing rules, I think that its interpretation in terms of clock times is so compelling that it probably has theoretical significance as well. Wolfgang Jeltsch has described a Curry-Howards isomorphism from FRP to temporal linear logic, and I think that the Moment
monad may enter this picture as well. It corresponds to the ability to embed the category of Haskell values into the category of temporal values. This is important if you want to use FRP within Haskell, otherwise you end up with a separate domain specific language. In other words, it allows us to mix temporal logic with standard lambda calculus.
Alight, that’s everything I wanted to say about APIs for dynamic event switching today. As always, I appreciate your comments.