Introducing Reactive: Events
Reactive is Conal Elliott’s newest FRP (functional reactive programming) framework. In this series of blog posts, I’m going to introduce a set of examples by concept. These articles are intended to serve as tutorials and motivation for writing interactive programs using Reactive instead of the IO monad.
A basic knowledge of Haskell and the prelude functions, especially higher order functions like
const, is all that is required.
We’ll start our journey by taking a look at events. Semantically, Events can be thought of as lists of time/value pairs where the times are non-decreasing:
Some varied examples:
[(1.0, 'h'), (2.0, 'e'), (3.0, 'l'), (3.1,'l'), (4.0,'o')]could be a keyboard event (
Event Char) where the user typed “hello” pressing the keys at those times.
[(50.0,FSharp), (50.0,ASharp), (50.0,CSharp)]could refer to three notes being plucked on a guitar at time 50.
- [(, ()), (0, ()), (5, ())] is an event that has an occurrence that happens before time. Note that events stretch time to include values at and . This is what the carot (^) signifies in the definition of above.
[ (1,()), (0,()) ]would not be an event since the times are decreasing.
Although we think of Events as lists, they are not implemented directly as them. Also, Events are abstract in that the representation itself isn’t manipulated directly. We’ll be using many Event functions that also apply to lists, and some that are specific to Events.
Some simple examples
Our examples are going to model various machines that have one button and a speaker that emits one sound. The button event is going to be of type
Event (). We’re using the () type since these events don’t carry any extra information, unlike a keypress event. Our sound event is going to be similarly represented as
Event (). So, quite simply our machines are all going to have the following type:
type BellMachine = Event () -> Event ()
For our first example, lets implement a simple machine that emits the bell event whenever the button event occurs.
doorBell :: BellMachine doorBell = id
Using reactive, we can implement machines much more complex than a doorbell while retaining the same high-level of simplicity demonstrated in the above code.
Consider another simple machine. This one sounds a bell 3 minutes after it is turned on. Before we implement it, lets take a look at what functions Reactive includes for generating new events:
-- Converts a list of time/value pairs into an event. Note that the times _must_ be non-decreasing. listE :: [(TimeT,a)] -> Event a atTimes :: [TimeT] -> Event () atTime :: TimeT -> Event ()
Assuming that time 0 is when the machine turns on, the bell event could be implemented like this:
-- Convert minutes to seconds mToS :: Double -> Double mToS = (*) 60 -- The appropriate time to cook a soft-boiled egg is 3 minutes (see wikipedia) eggTimer :: Event () eggTimer = atTime (mToS 3)
eggTimer isn’t a machine of our initial type; It lacks any mention of an attached button. We make it into a machine of the appropriate type by ignoring all button presses using
eggTimerM :: BellMachine eggTimerM = const eggTimer -- Alternative Definition -- eggTimerM = pure eggTimer
Lets look at a machine that sounds the bell at 10 seconds and when the user presses the button. Reflect, for a moment, on how this would be implemented imperatively. Timers? Signals? Event Loops? Compare that sketch to the simplicity of, and code-reuse within, the following reactive definition.
nifty :: BellMachine nifty button = eggTimer `mappend` button -- alternate definition -- nifty = mappend eggTimer
We can think of
mappend as merging events. Note that this function is part of the monoid concept and that
mappend’s counterpart, represents an event that never occurs. Taking this into consideration, we can implement a completely silent machine like this:
silent :: BellMachine silent = const mempty -- Alternative Definition -- silent = pure mempty
(Note: To use mappend and mempty, you’ll need to import Data.Monoid)
A more difficult example (fmap/switchE) (Functor/Monad)
Now lets implement a tricky machine. When the user presses the button twice, the machine begins pulsing the bell with a period equivalent to the time between the button presses. Another press of the button will stop the ringing at which point the process may be repeated.
Lets try to break the implementation into pieces. We know that we’ll need to calculate the difference of time between two event occurrences. For this sub-problem we’ll combine two of reactive’s functions, the first being
-- Combine an event instance's data with its corresponding time. withTimeE :: Event a -> Event (a, TimeT)
withTimeE is typical of reactive’s built-in functions in that it carries along the input Event’s data in the first element of the resultant tuple, while the computed value gets inserted into the second element of the tuple. If we instead wanted to ignore the initial event’s data, we could define a new function,
withTimeE_, as follows:
withTimeE_ :: Event a -> Event TimeT withTimeE_ e = fmap snd (withTimeE e) -- Alternative definition -- withTimeE_ = (fmap.fmap) snd withTimeE
fmap allows us to apply a function to all the values of an event.
mappend/mempty, works similarly on other data types including functions (see the Functor class defined in Control.Monad).
The function we’ll be using in conjunction with
withPrevE couples each value in an event with the previous occurrence’s value.
withPrevE :: Event a -> Event (a, a)
withTimeE, it adds the computed value (in this case, the previous value) to the
snd part of the tuple.
Combining these two functions, we can transform an
Event () into an
Event TimeT that represents the time passed since the previous event.
elapsedFirstTry :: Event () -> Event TimeT elapsedFirstTry e = fmap f (withPrevE (withTimeE_ e)) where f (tCur, tPrev) = tCur - tPrev -- alt. def. -- elapsedFirstTry = fmap (uncurry (-)) . withPrevE . withTimeE_
The above definition is all right, but in the spirit of reactive’s composability, we’ll define a more generally useful version that carries along the input event’s data and one that ignores it.
withElapsed :: Event a -> Event (a, TimeT) withElapsed = fmap f . withPrevE . withTimeE where f ((aCur, tCur), (aPrev, tPrev)) = (aCur, tCur-tPrev) withElapsed_ :: Event a -> Event TimeT withElapsed_ = (fmap.fmap) snd withElapsed
withElapsed gets is pretty far along the way of implementing the proposed machine, but we still have a ways to go. Observe that we only care about the elapsed time every 3rd time the button is pressed. To ease identification of these particular presses, we’ll label the event occurrences with a state. First, we’ll define our state data type and a method to cycle them.
data State = Idle | WaitingForPeriodEnd | PlayingCycle -- Given a state returns the next in sequence next :: State -> State next Idle = WaitingForPeriodEnd next WaitingForPeriodEnd = PlayingCycle next PlayingCycle = Idle
Reactive includes a simple function that will do the Event labeling for us.
-- | State machine, given initial value and transition function. Carries -- along event data. mealy :: s -> (s -> s) -> Event b -> Event (b,s) mealy_ :: s -> (s -> s) -> Event b -> Event s
Here is what we have so far . . . we’re getting pretty close.
soFar :: Event () -> Event (TimeT, State) soFar = mealy Idle next . withElapsed_
How do we generate the pulse events for the
PlayingCycle state? For this type of situation, reactive has a few functions that have the signature
Event (Event a) -> Event a. The most general of these is called join which applies to all Monads. In our case we’ll be using the
-- | Switch from one event to another, as they occur. switchE :: Event (Event a) -> Event a
switchE we’ll need to fmap our Event (TimeT, State) into an Event (Event ()). But first, lets define a function that will generate our periodic alarms:
period :: TimeT -> TimeT -> Event () period delta t0 = atTimes (iterate ((+) delta) t0)
And now we put it all together:
metronome :: BellMachine metronome = switchE . fmap f . withTimeE . mealy Idle next . withElapsed_ where f ((dt,PlayingCycle), t) = period dt t f _ = mempty
More to come
Our examples using Events are just the tip of the iceberg of reactive’s potential. In the next article, I’ll be explaining reactive’s Behaviors, which can be thought of as functions of time.