In their seminal paper Functional Reactive Animation, Conal Elliott and Paul Hudak introduced a function
untilB :: Behavior a -> Event (Behavior a) -> Behavior a
that can switch between different behaviors dynamically. In a more recent paper, Conal has renamed the function to switcher
. Semantically, it can be defined as
switcher b eb = join (stepper b eb)
where
join bb = \time -> (bb time) time
stepper x ex = \time ->
last $ x:[x' | (time',x') <- ex, time' < time]
My first design decision for the reactive-banana library was to remove this function from the set of combinators. In this post
switcher
leads to time leaks.switcher
is an acceptable loss of expressivity,By the way, it’s not just the switcher
function that is affected, the same reasoning applies to the join
function, i.e. the monad instance for Behavior
.
My main reason for removing the switcher
function is that it is prone to time leaks. Consider the example
e :: Event Int
e = ... -- some external event
f :: Int -> Behavior Int
f v = accumB v ((+1) <$ e)
bbad :: Behavior Int
bbad = switcher (const 0) $ fmap (\v -> f v) e
The behavior bbad
is a series of behaviors f v
whose starting value v
depends on the event e
. However, these behaviors also accumulate the event e
, which means that they depend on the history of the event e
. But since the behaviors are generated dynamically, we can’t update them in advance, we have to store the history of the event e
to be able to replay it on demand.
In general, any situation where we have to store the whole history of an event is called a time leak. Of course, an efficient implementation would never need to to do that; only the values at the immediately previous moment should matter. I am convinced that this should be a basic requirement on every FRP implementation.
To avoid the time leak problem, I simply decided to not implement dynamic event switching in reactive-banana. But what do we lose in terms of expressivity?
It turns out that it’s actually not so bad; many cases that seem to require dynamic event switching can actually be described in terms of the Applicative
instance and the apply
function. The point is that one can often determine a static set of behaviors in advance and then switch between them. Here an example:
eswitch :: Event (a -> a -> a)
behavior1or2 =
stepper (curry snd) eswitch <*> b1 <*> b2
Compare this to an implementation in terms of dynamic event switching
behavior1or2 =
switcher b2 ((\f -> f b1 b2) <$> eswitch)
Of course, this doesn’t work for dynamic collections of behaviors. However, you can always consider a time-varying collection Behavior [a]
instead of a collection of time-varying values [Behavior a]
. That’s what I currently do in my BlackBoard program which stores a collection of slides.
I now want to summarize two approaches to dynamic event switching that avoid time leaks.
A first idea is to naively ignore the history of the event e
and only accumulate from the time that the behavior is “created”. But this opens another can of worms, because any expression like accumB v ((+1) <$ e)
is now context-dependent. In other words, it now denotes different behaviors depending on how much of the history of the event e
is thrown away. For instance, the expression f 5
in
bbad2 = (+) <$> f 5 <*> bbad
denotes a different behavior than the expression f 5
that will occur inside bbad
.
You can make sense of this phenomenon by declaring that f 5
actually denotes a behavior generator, i.e. that it can “generate” many different behaviors at different starting times. However, the main problem is that we still need a way to denote behaviors that are context-independent. Naively turning everything into a generator does not work.
Nonetheless, Gergely Patai has managed to create a solution along these lines. The key point is that the context is made explicit in the type system, via a monad Context
(named StreamGen
in his paper). The type of accumE
will be
accumE :: a -> Event (a -> a) -> Context (Event a)
You can think of Context
as being the reader monad
type Context a = Time -> a
The idea is that if you have a value x :: Context (Behavior a)
and a time t :: Time
, then the value x t
denotes the behavior that “starts at time t
”. For example, the behavior accumE 0 e t
only accumulates occurrences of the event e
that happen after t
. (This is what avoids time leaks.)
The monad structure of Context
allows you to enforce the same context. For instance, the do
notation
do
e1 <- accumE 0 e
e2 <- accumE "" (((++) . show . ($ 0)) <$> e)
ensures that the events e1
and e2
always accumulate the same portion of the input event e
. (In a sense, Context
is just a glorified let
statement.)
The type of the switcher
function now becomes
switcher :: Behavior a -> Event (Context (Behavior a)) -> Behavior a
In other words, the event returns a context-dependent behavior. The point is that it is now impossible to generate a new behavior that depends on previous history: either the behavior depends on history, then it was already part of the network, or the behavior is newly generated with accumE
, then it doesn’t depend on any history.
There is another solution, first described by Wolfgang Jeltsch.
Again, the idea is that the type of switcher
should be changed so that it becomes impossible to access old history. This time, however, we don’t want to change the type of accumE
. How to achieve that?
Consider the code
b1 = accumE 0 e
b2 = switcher (pure 0) (expression)
In order to avoid time leaks, we have to make sure that the variable b1
cannot be used inside the expression
, we have to restrict its scope. This seems neigh impossible, but there is a precedent for that: the ST monad.
The idea is to augment the types Behavior
and Event
with an additional type parameter, aptly called era
:
type Behavior era a = Time -> a
type Event era a = [(Time,a)]
Think of era
as a type-level time interval. By using universal quantification over era
switcher :: Behavior era a
-> Event era ( ∀era'. Behavior era' a )
-> Behavior era a
we can ensure that the behavior b1
from above cannot be used inside the second argument of switcher
.
Unfortunately, this version of switcher
is pretty useless, because now we can’t bring anything into scope. The solution is to give switcher
the following type:
switcher :: Behavior era a
-> Event era
(∀era'. Behavior era' a -> ... -> Behavior era' a)
-> Behavior era a -> ... -> Behavior era a
This is to be understood in the sense that for each number of additional arguments, there exists a switcher
function with this type. The idea is that switcher
function itself brings variables into scope: if you want to use a variable inside the second argument, you first have to pass it through switcher
, like this
b1 = accumE 0 e
b2 = switcher (pure 0) (\b1' -> expression) b1
In particular, switcher
will throw away old history before making an event or behavior available.
Alright, I hope this survey was useful for setting the stage on time leaks and dynamic event switching.
Concerning reactive-banana, I don’t plan to include dynamic event switching in the immediate future. At the moment, I’m more interested in new abstractions for GUI programming. In particular, I’m thinking about introducing a third data type, a hybrid between Behavior
and Event
.