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 id and 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:

\mbox{\bf type } E_a = [(\hat{T},a)]

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.
    • [(-\infty, ()), (0, ()), (5, ())] is an event that has an occurrence that happens before time. Note that events stretch time to include values at -\infty and +\infty. This is what the carot (^) signifies in the definition of E_a 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)

Note that 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 const.

eggTimerM :: BellMachine
eggTimerM = const eggTimer
-- Alternative Definition
-- eggTimerM = pure eggTimer

mempty/mappend (Monoid)

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 mempty, 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 withTimeE

-- 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. fmap, like 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 withTimeE is withPrevE. withPrevE couples each value in an event with the previous occurrence’s value.

withPrevE ::  Event a -> Event (a, a)

Similar to 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 switchE.

-- | Switch from one event to another, as they occur.
switchE :: Event (Event a) -> Event a

To use 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.

continue on

Acknowledgments

This tutorial was created by David Sankel and Conal Elliott with important input from Thomas Davie and was sponsored by Anygma

Comments (13)

Kevin BallardNovember 8th, 2008 at 7:48 am

((fmap.fmap) snd withTimeE) seems overly complicated. Certainly was hard to read for someone who’s not used to working on ((->) r). ((fmap snd) . withTimeE) seems much simpler. Same thing with withElapsed_

It would also be nice to mention why you didn’t just use Enum for the State transitions (the answer being because Enum isn’t circular, but it took me a moment to realize that). Of course, if you were to derive Enum, Bounded, and Eq, then you could write a slightly shorter next with something like

next :: State -> State next x | x == maxBound = minBound | otherwise = succ x

Of course, this doesn’t seem particularly worthwhile here, but it would be useful if you needed more states. Just a thought.

Sasha RushNovember 10th, 2008 at 12:17 am

Sweet article. Reactive is really promising.

I wanted to actually build your machine, so I bugged Conal to add a way to adapt events to IO. Here’s the code I tried.

runMachine :: BellMachine -> IO () runMachine machine = do — get button press clock <- makeClock (buttonPressed, buttonPressedSink) > sink ())

main = runMachine $ doorBell

This guy works fine for the basic machines but it breaks for metronome. The first reason seems to be that when it gets to a point when there are no future events, switchE fails.

“Exception: Future mempty: it’ll never happen, buddy”

I could hack around this, but then I got to another issue. It seems like switchE is too lazy, which causes my events always fire one click behind when I press the button. My guess is that since switchE is implemented with withRestE, it needs to peak ahead one event.

Not sure how to deal with these issues in practice. Curious if I’m just doing something wrong.

adminNovember 10th, 2008 at 11:18 am

Kevin, thanks for your thoughts on Enum and for pointing out the ((fmap.fmap) snd withTimeE) style. Conal told me that he’ll be blogging about (fmap.fmap…) soon, but I’ll try to illustrate the reasoning behind it here.

Lets say we write a state monad that we’re happy with and now want to compose it with other monads. Unfortunately, we’ll have to rewrite our monad as a monad transformer before we can do that.

This lack of composability does not apply to Functors nor their extension, Applicative Functors. So the (fmap.fmap) format you recognized is how we use the composability of Functors. Check out this bad boy from the reactive source code:

-- | State machine, given initial value and transition function.  See also
-- 'stateE'.
stateE_ :: Ord t => s -> (s -> s) -> EventG t b -> EventG t s
stateE_ = (fmap.fmap.fmap.fmap) snd stateE

The alternative you suggested works fine in the cases you mention. When we get to Behaviors, and are now working with applicative functors, the (x.x.x) syntax really comes in handy. Lets consider adding two behaviors generated from user input:

add' :: (Ui -> Behavior Double) -> (Ui -> Behavior Double) -> (Ui -> Behavior Double)
add' a b ui = liftA2 (+) (a ui) (b ui)

And then we factor out the ui part:

add' :: (Ui -> Behavior Double) -> (Ui -> Behavior Double) -> (Ui -> Behavior Double)
add' a b = (liftA2.liftA2) (+) a b
-- or
add' = (liftA2.liftA2) (+)
adminNovember 10th, 2008 at 11:38 am

Sasha. I also wrote some tests on the bell machines and I’m getting similar results. Unfortunately, reactive is a bit buggy at the moment and has some subtle laziness issues (I suspect it’s due to my implementation of joinE, btw).

I don’t understand the code you posted above and I suspect it may be due to formatting issues. The reactive mailing list is a good place to report the behavior you’re seeing.

Later in this sequence of tutorials I’ll be covering the adaptation of reactive to IO in depth.

Sasha RushNovember 10th, 2008 at 10:38 pm

hey, I think my formatting got screwed up above, trying agin.

runMachine :: BellMachine -> IO () runMachine machine = do — get button press clock < - makeClock (buttonPressed, buttonPressedSink) >>= sink ()) main = runMachine $ metronome
Michael SmithNovember 12th, 2008 at 1:43 am

I tried something similar to Sasha, with similar results. I got around the future mempty error using a gigantic hack equivalent to:

fakeEmpty = atTime 9999999

and using this in the place of mEmpty. This “worked”, but I have some other problems… they could be laziness problems in Reactive or they could be my fault. My initial attempt at an IO adapter is a bit more complex than Sasha’s:

adaptEvent :: Sink (Event String -> Event (IO ())) adaptEvent f = do { clock <- makeClock; (textIn, textInSink) IO () doInput sink = do { l <- getLine; sink l; doInput sink; }

This is a very cool piece of software; my mind keeps jumping at possibilities that I don’t have time to implement. It’s great to see something like this being actively developed. I eagerly await a time when this is stable enough to try some more complex things…

Michael SmithNovember 12th, 2008 at 1:45 am

Wow, this blog really butchers Haskell in comments…

MichaelNovember 18th, 2008 at 10:30 pm

Please don’t keep us waiting much longer for part two on Behaviors!

David SankelNovember 19th, 2008 at 11:02 am

For those who are following, the issue with running these examples is in reactive’s trac system here.

Michael, the behavior’s part of the tutorial is going to cover a lot more ground than the above tutorial. It’s coming right along! ;-)

Koh Wei JieMarch 4th, 2009 at 11:41 pm

It does’t work-

$ ghci EventTut.hs GHCi, version 6.10.1: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim … linking … done. Loading package integer … linking … done. Loading package base … linking … done. [1 of 3] Compiling ConsoleAdapter ( ConsoleAdapter.hs, interpreted )

ConsoleAdapter.hs:24:50: Couldn’t match expected type Event Char' against inferred typeFRP.Reactive.Internal.Misc.Sink a’ In the first argument of f', namelystdinEvent’ In the second argument of fmap', namely(f stdinEvent)’ In the second argument of forkE', namely(fmap putChar (f stdinEvent))’ Failed, modules loaded: none.

Any help would be appreciated :)

Koh Wei JieMarch 5th, 2009 at 1:43 am

The code is here.

http://hpaste.org/fastcgi/hpaste.fcgi/view?id=2109#a2109

After changing

         (stdinEvent, stdinFeed) <- makeEvent clock

to

         (stdinFeed, stdinEvent) <- makeEvent clock

it compiles.

Now to figure out how to use this stuff.

MathijsJuly 25th, 2009 at 7:53 pm

I’m using reactive 0.11 on ghc 6.10.3 I used the above hpaste link to get started. I had to flip the event and the sink like mentioned above, and in adition to that, I had to replace ‘exact’ with ‘exactNB in ConsoleAdapter. It compiles OK, but when trying to run a machine (main = adapt metronome), one thread grabs 100% cpu and memory-use quickly goes up. nothing happens when I press space.

I would really like to get a bit more familiar with FRP. Any patches to fix this issue?

Thanks

NathanJanuary 7th, 2010 at 3:34 am

Hey… your function arrows in types are rendered as > Thanks for writing this I’m only a little way in and finding it very interesting.

Leave a comment

Your comment