FRP made simple!
Functional Reactive Programming refers to a declarative way to write eventful program in functional languages like Haskell. While the concepts behind FRP are straightforward to grasp, the actual implementations are not.
Libraries like sodium, reactive-banana, elerea, and yampa share one thing in common: large code bases that are not easily understood by beginners. In this blog post, I will develop a sodium-like FRP implementation for Haskell in under 200 lines. Note that this is possible by eschewing any concerns over performance or memory usage, so this library would probably not be practical to use in any live code.
A brief recap of FRP
FRP libraries usually consist of two primitives: Event
and Behavior
. An Event
refers to values
which only occur at certain times, and which are lost after the moment they occur. You can think of
an Event a
as a list of pairs of times t
and values a
: [(t, a)]
. A Behavior
is a
continuous value. Here, continuous means that a Behavior
has a defined value for any given time
(not the mathematical definition!). We can think of a Behavior a
as a function from a time t
to
the value a
.
First steps
The first thing we will do is define a monad for all of our FRP computations. Our FRP implementation
will also allow us to execute arbitrary IO
actions in response to events (much like the execute
primitive in Sodium), so we will need a separate monad to represent what happens “in the moment.”
Right now, we’ll just make both of these monads aliases for IO
.
Also, before we start, the final library we will end up writing is available on GitHub.
type React = IO
type Moment = IO
In many FRP implementations, Behavior
actually isn’t anything more than a step function whose
value can only be updated by events. Therefore, we’ll start by defining our Event
type. The only
really important thing about events is that we be able to subscribe and unsubscibe from them.
type RegisterEventListener a = (a -> Moment ()) -> IO (IO ())
newtype Event a = Event { _eventRegisterListener :: RegisterEventListener a }
First, we define the RegisterEventListener
type. Functions of this type can be used to register a
function in the Moment
monad which will be called whenever a new value (of type a
) of the event
is produced. The listener must be registered in the IO
monad, and the registration function
returns another monadic action of type IO ()
. This function is used to deregister the listener
that has been added.
Event a
fills the requirements for both a Monoid
and a Functor
. For the Monoid
instance,
mempty
is the Event that never fires, and mappend
takes two Event a
’s and returns a new Event a
that fires whenever either of the first two fires.
instance Monoid (Event a) where
mempty = never
mappend = merge
never :: Event a
= Event (\_ -> return (return ()))
never
merge :: Event a -> Event a -> Event a
= Event (\listener -> do
merge a b <- _eventRegisterListener a listener
unregisterA <- _eventRegisterListener b listener
unregisterB return (unregisterA >> unregisterB))
Let’s look at each function individually. First, let’s consider the mempty
or never
functions. Whenever a listener tries to register with this event, the listener is totally ignored
and the deregistration function returned does nothing. This means that the listener will never be
called (i.e., the event will never fire), which is the behavior we wanted.
The mappend
or merge
function take two events and returns a new one. When a listener tries to
register with the new event, the listener is in fact registered with both events. Thus, the listener
will be called whenever either a
or b
fire, which is the behavior we wanted. The deregistration
function deregisters the listener from both a
and b
.
Now for the Functor
instance. The Functor
instance for event allows us to apply functions to the
value contained inside an event in order to a get a new dependent event. I’ve included a type
signature to remind the reader of the type of the fmap
function for Event
’s.
instance Functor Event where
fmap :: (a -> b) -> Event a -> Event b
fmap f eb = Event (\listener -> _eventRegisterListener eb (listener . f))
In fmap
, we translate our listening function to be able to listen to the original event, and then
subscribe it to that. Recall that eb
has type Event b
, so _eventRegisterListener eb
has type
(b -> Moment ()) -> IO (IO ())
. The listener
function we’ve been supplied has type a -> Moment ()
, but listener . f
has type b -> Moment()
, so we can use it as the new listener for eb
.
Getting behaviors to behave
As we said before, although we can think of Behavior
s as continuous-time properties, in reality,
our FRP implementation will treat them as stepper functions that must be updated by an
event. Therefore, every Behavior
will need to be tied with an event that will fire whenever the
behavior is updated. Secondly, although the value of Behavior
is impure (it changes through time),
its value is well defined given a point in time. Therefore, we should be able to access it through
Moment
. Thus, our Behavior
type will also have to support a way to get at its current value.
It’s easy to combine these requirements into a Behavior
data type, which consists of an event
which fires on updates and a function to get the current value.
data Behavior a = Behavior
_behaviorUpdates :: Event ()
{ _behaviorGetValue :: Moment a } ,
It turns out that Behavior
is an Applicative
. The pure
function for Behavior
will return a
Behavior
whose value does not change in time. The (<*>)
function will return a Behavior
whose
value at any given point in time is the current value of the first Behavior
applied to the current
value of the second. I’ve included type signatures for convenience.
import Control.Applicative
instance Applicative Behavior where
pure :: a -> Behavior a
pure a = Behavior { _behaviorUpdates = mempty
= return a}
, _behaviorGetValue (<*>) :: Behavior (a -> b) -> Behavior a -> Behavior b
<*> ba = Behavior { _behaviorUpdates = _behaviorUpdates bab <> _behaviorUpdates ba
bab = do ab <- _behaviorGetValue bab
, _behaviorGetValue <- _behaviorGetValue ba
a return (ab a) }
The Behavior
returned by pure a
is a Behavior
that never updates (i.e., is constant) and whose
value always returns a
. This fulfills our requirements for a constant Behavior
.
The Behavior
returned by f <*> a
is a Behavior
who updates whenever f
or a
update, and
whose value is the value of f
applied to the value of a
.
Interfacing with the real world
So now we have Event
and Behavior
data type and methods to combine them with other Event
’s and
Behavior
’s respectively. What we don’t have is any way to make an Event
that actually fires, or
a Behavior
that is not built from other Behavior
’s. Let’s implement this functionality to make
our library actually useful.
Putting the ‘R’ in FRP
Most FRP implementations offer the user a way to create an event as well as an IO (or equivalent)
function that can be used to trigger that event. Our library is no different. Let’s define a
newEvent
function which will return a new Event
as well as a function to trigger our Event
.
import Control.Monad.Trans -- for `liftIO`
import qualified Data.Map as M
import Data.Unique
import Data.IORef
newEvent :: React (Event a, a -> Moment ())
= do (registerListener, propagateListeners) <- newEventRegistration
newEvent return (Event registerListener, propagateListeners)
newEventRegistration :: React (RegisterEventListener a, a -> Moment ())
= do listeners <- newIORef M.empty
newEventRegistration let registerListener listener =
do listenerKey <- newUnique
modifyIORef listeners (M.insert listenerKey listener)return (modifyIORef listeners (M.delete listenerKey)
=
propagateListeners x do listeners' <- M.elems <$> liftIO (readIORef listeners)
mapM_ ($ x) listeners'
return (registerListener, propagateListeners)
I won’t go into all the details here, but basically, the sausage is made in the
newEventRegistration
function which uses an IORef
to keep track of registered listeners and a
Unique
from the Data.Unique
library to give each listener a unique key, which is useful for the
deregistration function.
Hold your horses!
So now we can make an Event
that we can fire by simply calling a function, but how do we make a
Behavior
? Simple! Since Behavior
’s are just steppers updated by events, we’ll write a function
that will take an Event a
and give us a Behavior a
. However, because the Event a
might not
fire immediately, we’ll need to give the Behavior a
an initial a
to hold in the mean-time.
import System.Mem.Weak
hold :: a -> Event a -> React (Behavior a)
=
hold initial updates do cell <- newIORef initial
let behavior = Behavior { _behaviorUpdates = () <$ updates
= liftIO (readIORef cell) }
, _behaviorGetValue <- _eventRegisterListener updates (\x -> writeIORef cell x)
unregisterUpdates
addFinalizer behavior unregisterUpdatesreturn behavior
Again, I won’t go into too much detail here, but basically we create an IORef
to store the current
value of the behavior, which we read out in the sampling function. The behavior updates event is
made by creating an Event ()
from the Event a
supplied to us (i.e., the behavior will update
whenever the event does). The (<$)
function is given to us for free by virtue of Event
being a
Functor
. Finally, we register a listener for updates to the updating event, and update the cell
appropriately. We use the addFinalizer
function from System.Mem.Weak
to automatically unregister
our listener when behavior
is garbage collected.
There is one more consideration that we’re not taking into account here. Because we write to the
IORef
immediately in the listener, it is possible that sampling the behavior at different points
in the Moment
monad will give us different results. This is not what we want since a computation
in the moment monad should semantically run “at the same time.” What we really want is for the cell
to be written after the current moment is complete. We can do this by updating our definition of the
Moment
monad.
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.Writer hiding (listen)
newtype Moment a = Moment { runMoment :: WriterT [IO ()] IO a }
deriving (Monad, Applicative, Functor, MonadWriter [IO ()], MonadIO, MonadFix)
hold :: a -> Event a -> React (Behavior a)
=
hold initial updates do cell <- newIORef initial
...
<- _eventRegisterListener updates (\x -> tell [writeIORef cell x])
unregisterUpdates ...
We will also need a function that can run a Moment a
inside IO
.
import Control.Monad
sync :: Moment a -> IO a
= do (a, updateHolds) <- runWriterT (runMoment m)
sync m sequence_ updateHolds
return a
Note that we will also have to update our use of Moment
elsewhere, including adding liftIO
’s as
necessary. All of these changes are made in the GitHub source.
Wrapping things up in the real world
Finally, let’s create some utility functions to listen to Event
’s and Behavior
’s, thus
completing our interactions with the real world. This will also allow us to keep our implementations
of Event
and Behavior
opaque.
listen :: Event a -> (a -> Moment ()) -> IO (IO ())
= _eventRegisterListener
listen
-- The listener here is called once to observe the behavior's initial value
-- and subsequently on updates. In some ways, it breaks the abstraction of
-- a continuous-time value, but it's a concession to practicality
listenToBehavior :: Behavior a -> (a -> Moment ()) -> IO (IO ())
= do sync $ do initial <- sample bb
listenToBehavior bb handle
handle initial-> sample bb >>= handle)
listen (_behaviorUpdates bb) (()
sample :: Behavior a -> Moment a
= _behaviorGetValue sample
Again, the functions here are pretty self-explanatory, except perhaps listenToBehavior
. Basically,
we call the handler once with the initial sampled value of bb
and then we register to updates from
bb
and call the handler using the value of bb.
This all looks good, but there’s a problem with sampling bb
in the _behaviorUpdates bb
handler. Remember how we took care to make sure that Behavior
’s do not update until the current
Moment
is complete? This means that when we sample bb
here we will be getting the old
value. That’s not what we want! To remedy this, we’ll use the same trick we used with hold
. We
will modify Moment
to keep track of IO actions to run after all the holds have been updated.
newtype Moment a = Moment { runMoment :: WriterT ([IO ()], [IO ()]) IO a }
deriving (Monad, Applicative, Functor, MonadWriter ([IO ()], [IO ()]), MonadIO, MonadFix)
listenToBehavior :: Behavior a -> (a -> Moment ()) -> IO (IO ())
= do sync $ do initial <- sample bb
listenToBehavior bb handle
handle initiallet handle' = sync (sample bb >>= handle)
-> tell ([], [handle']))
listen (_behaviorUpdates bb) (()
sync :: Moment a -> IO a
= do (a, (updateHolds, afterHolds)) <- runWriterT (runMoment m)
sync m sequence_ updateHolds
sequence_ afterHolds
return a
Again, previous Moment
usages will have to be updated to reflect this new structure, which has
been done in the GitHub source.
Some other primitives
Modern FRP libraries usually contain a few other primitives, which we’ll implement here. One of the
more important features is the ability to define accumulators, Event
’s whose value depends on
previous values. Let’s write the accum
primitive now.
Keeping state
accum :: a -> Event (a -> a) -> React (Event a)
=
accum initial updaters do cell <- newIORef
<- newEventRegistration
(registerListener, propagateListeners) let evt = Event registerListener
<- _eventRegisterListener updaters $ \updater ->
unregisterEventListener do cellValue <- liftIO (modifyIORef cell updater >> readIORef cell)
propagateListeners cellValue
addFinalizer evt unregisterEventListenerreturn evt
For accum
we create an IORef
to hold the current value, and then subscribe to the updaters
event directly. We must subscribe to updaters
here, which means that accum
must be run in the
React
monad. If we didn’t subscribe to updaters
immediately, then it’s possible that we will
miss some updates. We also play nice by registering a finalizer on the new event we create.
Using accum
and hold
we can make a Behavior a
whose current value depends on previous ones as
well.
accumB :: a -> Event (a -> a) -> React (Behavior a)
= do ea <- accum initial updaters
accumB initial updaters hold initial ea
Spilling the beans
reactive-banana
and sodium
also have a primitive that takes an event whose value is a list and
returns an event of just values that fires for each element in the list. This is called something
like spill.
spill :: Event [a] -> Event a
= Event (\listener ->
spill eas $
_eventRegisterListener eas -> mapM_ listener as) \as
Simply put, when someone listens to the spill
ed event, we subscribe to the Event [a]
and call
the listener for Event a
on each element of the list.
Calming down
spill
gave us the ability to have one event produce multiple simultaneous events, but what if we
don’t want to listen to all of those? What if we wanted to “un-spill
”? Luckily, there’s a
combinator for that, usually called calm
. It takes an event that may fire more than once in a
given Moment
and returns a new Event
that will only fire once for the first firing of the
event. We will need to modify the Moment
monad again to keep track of which events have already
been delivered.
import Control.Monad.State
import qualified Data.Set as S
newtype Moment a = Moment { runMoment :: StateT (S.Set Unique) (WriterT ([IO ()], [IO ()]) IO) a }
deriving (Monad, Applicative, Functor, MonadState (S.Set Unique), MonadWriter ([IO ()], [IO ()]),
MonadIO, MonadFix)
,
sync :: Moment a -> IO a
= do (a, (updateHolds, afterHolds)) <- runWriterT (evalStateT (runMoment m) S.empty)
sync m sequence_ updateHolds
sequence_ afterHolds
return a
calm :: Event a -> React (Event a)
= do key <- liftIO newUnique
calm evt let evt = Event $ \listener -> _eventRegisterListener evt (calmed listener)
=
calmed listener a do alreadyCalled <- S.member key <$> get
not alreadyCalled) $ do
when (
modify (S.insert key)
listener areturn evt
Basically, calm
adapts the listener to check if the listener has been called yet in this
moment. If it has not, then it calls the listener and notes in this Moment
that the listener has
been called. Sweet!
Switching things up
The most complicated combinator we’re going to write today is switchE
and switch
. These
combinators let you dynamically change the state of the network. The switchE
combinator takes a
Behavior (Event a)
and returns an Event a
that fires whenever the current Event a
in the
given Behavior
fires. This requires some trickery, but we actually don’t need to write this in the
React
monad.
switchE :: Behavior (Event a) -> Event a
= Event $ \listener ->
switchE be do eInitial <- sync $ sample be
<- newIORef (return ())
unregisterV <- _eventRegisterListener eInitial listener
unregisterListener
writeIORef unregisterV unregisterListenerlet switchToNewEvent =
do unregisterFromOld <- readIORef unregisterV
unregisterFromOld<- sync $ sample be
eNext <- _eventRegisterListener eNext listener
unregisterNewListener
writeIORef unregisterV unregisterNewListener<- _eventRegisterListener (_behaviorUpdates be) $ \() ->
unregisterBehaviorListener
tell ([], [switchToNewEvent])return $ do
<- readIORef unregisterV
unregisterEvtListener
unregisterEvtListener unregisterBehaviorListener
Basically, what this does is that, whenever a new listener subscribes, we subscribe to the current
Event a
contained in the behavior, as well as the Behavior
updates. The subscription to the
Event a
will call our listener for events on the initial event until the Behavior
updates. At
this point, the switchToNewEvent
is called after all the new behavior values have been written
in the holds (that’s why we use tell
to register an afterHolds
action). Because
switchToNewEvent
is called after the current Moment
, the listener will continue to be called on
firings of the old event until the end of the Moment
. This is what we want, since our convention
is that Behavior
’s do not update until the end of the current moment.
When switchToNewEvent
is called, we read the current unregistration IO action from the IORef
and
run it, thus disconnecting listener
from future firings of the old Event a
. We read the new
Event a
from the behavior and register listener
to listen to the new event instead. Finally, we
write out the deregistration function for the new event fo the IORef
. The deregistration function
for the dynamically switched event disconnects both the listener from the current event and
switchToNewEvent
from the behavior updates.
Whereas switchE
dynamically switches between Event a
’s, switch
dynamically switches between
Behavior
’s. Remember, we need to two things to define a behavior: an event that updates when the
behavior updates, and a way to read the current value. The switchE
gives us an easy way to express
the former, and it’s pretty straightforward to write a function to read the current value.
switch :: Behavior (Behavior a) -> Behavior a
= Behavior { _behaviorUpdates = switchE (_behaviorUpdates <$> bb)
switch bb = do b <- sample bb
, _behaviorGetValue sample b }
Conclusion
And that’s it! In this post, we’ve written a simple FRP implementation in less than 200 lines. Unlike it’s more complicated cousins, like reactive-banana or sodium, this implementation makes no performance or memory guarantees, and doesn’t even have very strict semantics, but you’ll find it works pretty intuitively for most things (TM). In the next tutorial, we’ll use our library to write a simple game. Stay tuned for more, and be sure to check out the GitHub source for React.Hs.