Dynamics
Posted on September 25, 2017Previously we had a look at Behaviors, which finished our coverage of the two main FRP types.
Now we’ll have a look at an additional tool that reflex gives us - Dynamics.
What is a Dynamic?
A Dynamic is the combination of an Event and a Behavior.
A Dynamic in reflex looks like this:
and we can think of it as being a pair of an Event and a Behavior:
The Behavior carries some state around, and the Event fires with the value of the new state whenever the state is changed. This is useful for reasons of efficiency. Behaviors are pull-based, which means we need to poll them for changes, so combining a Behavior with an Event that fires when it updates means that we can write code that reacts to changes in state at the time when the changes occur.
This is very useful when we want to update a piece of the DOM. A Dynamic t Text can be passed from where it was created, through the application, down to a DOM node that needs to display some changing text. The Behavior t Text takes care of tracking the current state of the text to display, and reflex-dom can set up some code to replace the text in the DOM node whenever the Event signals that there is a change.
The reflex and reflex-dom libraries aren’t prescriptive about how you structure your application, but the common advice is to pass Dynamics as far down as possible into the code that generates DOM. If you follow that advice then you can arrange things so that your code is doing the same changes to the DOM as the various virtual DOM libraries would, but skipping the need to diff and patch the various DOM trees.
There is some reference to building an Event and Behavior simultaneously in the reactive-banana documentation, and it appears in one of the examples, and so I suspect that the idea behind the Dynamic is probably lurking in the background or in the folklore of other Event-and-Behavior FRP systems.
Working with Dynamics
We can get hold of the Event if we need it:
and we can get hold of the Behavior:
We also still have the handy typeclass instances that were available for Behaviors:
instance Reflex t => Functor (Dynamic t) where ...
instance Reflex t => Applicative (Dynamic t) where ...
instance Reflex t => Monad (Dynamic t) where ...We have to construct them directly, rather than combining existing Events and Behaviors, so that the two components stay synchronized.
There is an equivalent of hold but for Dynamics:
and there is a variant that lets us fold a function through a series of Event firings:
foldDyn :: (Reflex t, MonadHold t m, MonadFix m)
=> (a -> b -> b)
-> b
-> Event t a
-> m (Dynamic t b)which we’ll often be using with the function application operator:
In order to unify:
and:
we need c ~ d.
We then have:
being unified with:
and so a ~ (c -> c) and b ~ c.
After the dust settles, we have:
The MonadFix constraint is being used because foldDyn uses value recursion internally.
An example of using Dynamics
We’re going to build a simple counter.
To do that we’re going to build a up a Dynamic t Int, which will start at 0 and will be transformed when various Events fire.
We can use foldDyn to get started:
We’ll add an Event corresponding to the pressing of an “Add” button:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> m (Dynamic t Int)
counter eAdd =
foldDyn ($) 0 $
_When that button fires, we’ll need to use that Event to supply a function of type Int -> Int:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> m (Dynamic t Int)
counter eAdd =
foldDyn ($) 0 $
_ <$ eAddand fortunately we have just the thing:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> m (Dynamic t Int)
counter eAdd =
foldDyn ($) 0 $
(+ 1) <$ eAddIt is really common in FRP systems like these to deal with Events which have functions for values, which is worth remembering when you’re starting out and trying to solve problems like these for the first time. It can seem really strange at first but you will get comfortable with it if you practice for a while and try to remember that functions are values too.
Let’s modify our counter so that we can reset it.
We’ll start with what we had before:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> m (Dynamic t Int)
counter eAdd =
foldDyn ($) 0 $
(+ 1) <$ eAddand add an Event that will fire when “Clear” is pressed:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter eAdd eClear =
foldDyn ($) 0 $
(+ 1) <$ eAddWe are planning on creating some buttons to produce these Events, and so the Events won’t happen simultaneously. However, since we have separated out the logic from the controls, and are taking Events as inputs, we can’t guarantee that the Events will be happening in different frames.
We are working with Event t (Int -> Int), so we’ll combine the Events using mergeWith and function composition:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter eAdd eClear =
foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, _
]Now we just need to put the Event in place:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter eAdd eClear =
foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, _ eClear
]and supply a suitable function:
counter :: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter eAdd eClear =
foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, const 0 <$ eClear
]So far, so good.
Bringing RecursiveDo into the picture
Behaviors have values at all points in time, and Events only have values at certain instants in time. This means that all Behaviors have a value before any Events in the application fire, and so any Event can be used to sample from a Behavior.
We can use one firing of a particular Event to build up a Behavior and a later firing of the same Event to sample a Behavior. Going further than this, we can use one firing of a particular Event to both build up and sample from a Behavior. The reason this is fine is that hold updates the Behavior in the next frame rather than the current frame.
The result of this is that we are able to have loops in the graph of our FRP network. This is fine, and is often very useful, but we need a language extension to be able to input them into Haskell in a convenient manner.
Imagine that we had an application that contained a counter that we wrote earlier, and that we wanted to specify an upper limit for the value of the counter.
We could add something like this to the settings page for the application:
limit :: (Reflex t, MonadFix m, MonadHold t m)
=> Event t ()
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
limit eStart eAdd eClear = do
eLoadLimit <- performEvent (loadLimitDb <$ eStart)
dLimit <- foldDyn ($) 5 . mergeWith (.) $ [
const <$> eLoadLimit
, (+ 1) <$ eAdd
, const 0 <$ eClear
]
performEvent_ (saveLimitDb <$> update dLimit)
return dLimitand then plumb the results into a revised form of our counter.
We would start with our old counter:
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter eAdd eClear = do
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, const 0 <$ eClear
]
pure dCountand then pass in the limit:
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Dynamic t Int
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter dLimit eAdd eClear = do
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, const 0 <$ eClear
]
pure dCountWe can then check if we are within the limit:
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Dynamic t Int
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter dLimit eAdd eClear = do
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, const 0 <$ eClear
]
let dLimitOK = (<) <$> dCount <*> dLimit
pure dCountand use that to create a version of eAdd which only fires if we are within the bounds of the limit:
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Dynamic t Int
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter dLimit eAdd eClear = do
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAdd
, const 0 <$ eClear
]
let dLimitOK = (<) <$> dCount <*> dLimit
eAddOK = gate (current dLimitOK) eAdd
pure dCountNow all we have to do is replace the use of eAdd in the foldDyn with eAddOK.
That is going to look a little weird and fail to compile, due to the cyclic dependency it introduces:
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Dynamic t Int
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter dLimit eAdd eClear = do
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAddOK
, const 0 <$ eClear
]
let dLimitOK = (<) <$> dCount <*> dLimit
eAddOK = gate (current dLimitOK) eAdd
pure dCountbut we can resolve this by adding the RecursiveDo language pragma and replacing the the do keyword with mdo:
{-# LANGUAGE RecursiveDo #-}
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Dynamic t Int
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter dLimit eAdd eClear = mdo
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ 1) <$ eAddOK
, const 0 <$ eClear
]
let dLimitOK = (<) <$> dCount <*> dLimit
eAddOK = gate (current dLimitOK) eAdd
pure dCountyou’ll see that it works, and that it is linked to the limit widget above.
If we hadn’t needed the MonadFix constraint in order to use foldDyn, we would need it now in order to use mdo.
We could take this further, and create a data type to manage the settings:
We can pull the settings apart for use in our counter:
counter :: (Reflex t, MonadFix m, MonadHold t m)
=> Dynamic t Settings
-> Event t ()
-> Event t ()
-> m (Dynamic t Int)
counter dSettings eAdd eClear = mdo
let
dLimit = settingsLimit <$> dSettings
dStep = settingsStep <$> dSettings
let check c s l = c + s <= l
dLimitOK = check <$> dCount <*> dStep <*> dLimit
eAddOK = gate (current dLimitOK) eAdd
dCount <- foldDyn ($) 0 . mergeWith (.) $ [
(+ ) <$> tag (current dStep) eAddOK
, const 0 <$ eClear
]
return dCountand we can build the settings up from the individual pieces on our hypothetical settings page:
We’re still playing with basic examples, but hopefully these examples plus a bit of imagination are enough to help see the usefulness of building up, passing around and pulling apart first-class values for state management.
Removing extraneous updates
There is a small trap here when we start decomposing our Dynamics.
Imagine that we constructed a Dynamic that keeps track of a pair of Colours:
dynPair :: (Reflex t, MonadHold t m)
=> Event t Colour
-> Event t Colour
-> m (Dynamic t (Colour, Colour))
dynPair eInput1 eInput2 = do
dColour1 <- holdDyn Blue eInput1
dColour2 <- holdDyn Blue eInput2
pure $ (,) <$> dColour1 <*> dColour2
and in some other part of our application we would like to break that pair apart into a pair of Dynamics:
splitPair :: Reflex t
=> Dynamic t (Colour, Colour)
-> (Dynamic t Colour, Dynamic t Colour)
splitPair dPair =
let
p1 = fmap fst dPair
p2 = fmap snd dPair
in
(p1, p2)This probably won’t do what we want.
Imagine that the first Event passed to dynPair never fires. Whenever the second Event passed to dynPair fires, the output Dynamic will update. If that output is passed through splitPair we’ll have a pair of Dynamics that are updating, although the first of these will have an Event firing that doesn’t correspond to a change in state.
This is particularly problematic if we’re trying to minimize the number of times we have to update a DOM tree.
We can see that in action here if we click back and forth between “Red” and “Blue” for one set of inputs:We can solve this by using holdUniqDyn:
to create a new version of splitPair:
splitPair :: (Reflex t, MonadHold t m, MonadFix m)
=> Dynamic t (Colour, Colour)
-> m (Dynamic t Colour, Dynamic t Colour)
splitPair dPair =
do
p1 <- holdUniqDyn (fmap fst dPair)
p2 <- holdUniqDyn (fmap snd dPair)
pure (p1, p2)This highlights an additional issue with our implementation of dynPair - if we clicked the same button over and over, we’d trigger updates even when the state wasn’t changing. If this was important to us we could address this by using holdUniqDyn within dynPair itself.
Playing along at home
If you want to test out your understanding of Dynamics, there are Dynamic-themed exercises here. These exercises build up incrementally as the series progresses, so it would probably best to start the exercises beginning at the start of the series.
Next up
We now have all the pieces that we need to build an FRP network.
In the next post we’ll start looking at how to create DOM elements using reflex-dom.
We’re preparing educational materials about the reflex library, and using it to see what exciting things we can do with FRP.
> Dave Laing
Dave is a programmer working at the Queensland Functional Programming Lab.