SwitchingPosted on September 29, 2017
Previously we looked at how to use
reflex-dom to put DOM elements on a page. Now we’re going to look at how to modify our FRP network in response to user input.
What is switching?
Everything we’ve done so far has involved building up an FRP network that is static. The graph of dependencies between
Dynamics is set up in a particular configuration and it stays in that configuration for the lifetime of the application. The same is true of our DOM elements - once we have laid them out on the page, they are there forever.
Sometimes we will want to modify the FRP network or the DOM in response to
Events, and that is what the various switching functions do.
There are two kinds of switching functions available to us:
- functions that modify the FRP network in response to input
- functions that modify the DOM in response to input
We’ll cover these one at a time.
When we want to modify an FRP network, we do it using higher-order FRP.
We’ve probably seen higher-order functions, which is where function which take functions as arguments, like:
Higher-order FRP involves an FRP type - one of
Dynamic - which contains another FRP type.
Some examples turn up due to the fact that
Monad instances, giving rise to:
There are other functions provided by
reflex that enable higher-order FRP, including:
It can be handy to think of a railroad switch while you’re getting use to these methods.
We’ll focus on
switch for a while to motivate higher-order FRP:
We can view a
Behavior t (Event t a) as an
Event t a which is varying over time. There could be multiple sources of these
Events, and the
Behavior is being used to track which
Event should be used at any given moment in time. Since
Behaviors have values at all points in time, that means there is always an
Event which is selected. The
switch function is giving us access to the
Event that is selected by the
This time, we’re going to look at the example before we look at the code:
Have a click around until you’re comfortable that you know what is going on.
We’re going to zoom in on the function that creates the
Event that is used as the input to the counter on the left-hand side.
It takes in an
Event for the “Add” button and both “Select” buttons.
We’re going to create a
Behavior t (Event t ()) so that we can use
switch to get the
Event that we want out of this:
We build up our
which you might recall has this type signature:
Since we’re calling
switch like we get an
Event t () out of it, this leads to
a ~ Event t () in the above, which translates to:
for this particular use of
We’ll make a note of that:
The “Add” button starts off being routed to the left input, so we’ll use that as our initial value for our
Behavior is going to be changed whenever either of the “Select” buttons is pressed. We use
leftmost and the
Events coming from the “Select” buttons to start building up the other input to
hold. This leaves us with two things to fill in, but we know their types:
leftInput :: (Reflex t, MonadHold t m) => Event t () -> Event t () -> Event t () -> m (Event t ()) leftInput eAdd eSwitchL eSwitchR = do -- beAddL :: Behavior t (Event t ()) -- _changeLeft :: Event t () -- _changeRight :: Event t () beAddL <- hold eAdd . leftmost $ [ _changeLeft <$ eSwitchL , _changeRight <$ eSwitchR ] pure (switch beAddL)
We also know that we want the input from the “Add” button to flow through to the output when the output on the left-hand side is selected, and that we want no input to flow through to the output when the output on the right-hand side is selected. We have those things at hand:
and we are done.
Switching the “Add” clicks towards the output on the right-hand side is very similar:
We can do this a little more directly using
The first argument is the initial
Event to use as the output. The second argument consists of an outer
Event and an inner
Event. The outer
Event fires to indicate that the output should switch. It switches to the value of the inner
Event until such time that the outer
Event fires again.
This results in:
If you’ve been paying close attention, you might be wondering if we could have just used
ffilter to do something like this. For this example, you certainly could.
The benefits of modifying the FRP network start to come into play once pieces of your network start to consume significant amounts of processing time or memory. In those cases, you can use switching to only add those pieces to your FRP network when you need them and to remove them once you no longer need them.
If some piece of the network becomes entirely disconnected from the rest of the network then it is eligible for garbage collection, and so you’ll be able to reclaim the memory it was using and rest easy knowing that it isn’t hanging around and using up your processing time.
Dynamic modifications to the DOM
Imagine that we have three widgets that all return an
Event t Text.
Eventwhen the user types:
Eventwhen the user clicks the buttton: and a tick widget with fires its
Eventevery second and doesn’t care about the user at all:
We’d like to switch between displaying these widgets and collect the
Event t Text from whichever widget is being displayed.
Faking it by hiding elements
Imagine that we are very keen on premature optimization, and we know that modifying the structure of the DOM is slow but modifying attributes on the DOM is fast.
We could solve this problem by hiding elements and gating their outputs based on whether or not they are displayed.
We start by putting a button on the page:
and we track how many times it has been toggled:
This is using a handy helper function from
reflex in passing:
to toggle a
Bool when an
We also set up a
Dynamic with the negation of that
Bool, because it will be handy in a moment:
We write a helper function to toggle the “hidden” attribute based on a
which we use to create a pair of
Dynamic attribute maps:
Now we can wrap our widgets in divs that will be hidden or shown based on how many times the button has been pressed:
We need to gate the outputs so that only
Events from the currently displayed widget flow through:
That output gets turned into a
Dynamic so that we can display it, which we clear whenever the “Switch” button is pressed:
after which we put it on the page:
Here it is in one piece:
hideWidget :: MonadWidget t m => m () hideWidget = el "div" $ do -- Set up a button to switch between inputs eSwitch <- el "div" $ button "Switch" -- Toggle a `Bool` when the button is pressed dToggle <- toggle True eSwitch let -- Track the opposite state as well dNotToggle = not <$> dToggle -- Hide elements based on a `Bool` mkHidden False = "hidden" =: "" mkHidden True = mempty -- Build a Dynamic class based on how -- many times "Switch" has been pressed dHide1 = mkHidden <$> dToggle dHide2 = mkHidden <$> dNotToggle -- Show the text widget for an even number of presses eText1 <- elDynAttr "div" dHide1 textWidget -- Show the text widget for an odd number of presses eText2 <- elDynAttr "div" dHide2 buttonWidget let -- Gate the outputs from the widget so that they don't step on each other eText = leftmost [ gate (current dToggle) eText1 , gate (current dNotToggle) eText2 ] -- Clear the output when switching occurs dText <- holdDyn "" . leftmost $ [ eText , "" <$ eSwitch ] -- Display the output el "div" $ dynText dText
we’ll see that we’re definitely not adding or removing elements from the DOM, because the state of the text input is being maintained across clicks of the “Switch” button.This is even more pronounced if we use the timer widget in place of the button widget:
Switching out elements
If want to have freshly laid out widgets every time we click the “Switch” button, we need some new functions.
The first of these is
The first argument is the initial widget to lay out on the page. The second argument is an
Event with the next widget to lay out on the page as its value. The values that these widgets return are collected into a
Dynamic. We’ll see why you would want this in a moment.
MonadAdjust typeclass is present here so that we can replace pieces of the FRP network.
You might wonder how we managed without this for the switching functions at the beginning of this post. If we think of an FRP network as a graph, the earlier switch function were moving edges around between nodes. We could completely remove a piece of the graph - with help from the garbage collector - if nothing is connected to it and if we know that nothing will ever be connected to it again, but that is the only way we could effect the nodes via switching. For all other cases where we want to add or remove nodes, we need the
MonadAdjust typeclass to place and connect new nodes when certain
In the same way that we have
never for when we need an
Event which doesn’t fire, we have
blank for when we need a widget that doesn’t display on the page. Sometimes that is useful as an initial value for
widgetHold, but not always.
With all of that out of the way, let us have a look at how we might use
We start with the same button and toggling
Dynamic that we used for
widgetHold we’re going to need
Events that trigger when we want to change widgets, so we set some up:
Now that we have the pieces in place, we use
widgetHold to put
textWidget on the page, and to switch between the two widgets depending on how many times the “Switch” button has been pressed:
This gives us a
Dynamic t (Event t Text), and we want an
Event t Text.
There is a function with this signature:
In our case, we use it to pull out an
Event t Text:
At that point we have what we need to display the output on the page as before:
All together it looks like:
holdWidget :: MonadWidget t m => m () holdWidget = el "div" $ do eSwitch <- el "div" $ button "Switch" dToggle <- toggle True eSwitch let -- This will fire when `dToggle` changes to `True` eShow1 = ffilter id . updated $ dToggle -- This will fire when `dToggle` changes to `False` eShow2 = ffilter not . updated $ dToggle -- Builds up a `Dynamic t (Event t Text)` -- Starts with `textWidget` deText <- widgetHold textWidget . leftmost $ [ -- Switch to `textWidget` when `dToggle` change to `True` textWidget <$ eShow1 -- Switch to `buttonWidget` when `dToggle` change to `False` , buttonWidget <$ eShow2 ] let -- Collapse the `Dynamic t (Event t Text)` to an `Event t Text` eText = switchDyn deText -- Clear the output when switching occurs dText <- holdDyn "" . leftmost $ [ eText , "" <$ eSwitch ] -- Display the output el "div" $ dynText dText
These are small examples, but the idea gets more useful as you do more adventurous things.
If we don’t know what we want to use as an initial value for
widgetHold, we can use
widgetHold is probably a better bet if you have a choice between the two.
To use it, we would start with something that should look very familiar:
We’ll build a
Dynamic of widgets that return
Event t Text:
and we’ll use
dyn to collect the outptuts into an
Event t (Event t Text):
We can collapse these to an
Event t Text using
never as the inital
and then we proceed as normal:
All in one place it looks like this:
dynWidget :: MonadWidget t m => m () dynWidget = el "div" $ do eSwitch <- el "div" $ button "Switch" dToggle <- toggle True eSwitch let eShow1 = ffilter id . updated $ dToggle eShow2 = ffilter not . updated $ dToggle -- Builds up a `Dynamic` of widgets that return `Event t Text`: dWidget <- holdDyn textWidget . leftmost $ [ textWidget <$ eShow1 , buttonWidget <$ eShow2 ] -- Using `dyn` on this gives us an `Event t (Event t Text)`: eeText <- dyn dWidget -- and we can use `switchHold` to turn that into an `Event t Text`: eText <- switchHold never eeText dText <- holdDyn "" . leftmost $ [ eText , "" <$ eSwitch ] el "div" $ dynText dText
We can use a handy piece of functionality to clean this up a little. It might look scary at first glance, but we’ll get used to it pretty quickly.
Workflow is a
newtype used to build a graph of widgets that the user will transition through. It’s the kind of thing you would reach for if you were building a “wizard” in a UI, but it is much more flexible than that.
newtype wraps a widget that returns a pair, containing the result we are interested in and an
Event which will fire with the next piece of the
Workflow we want to visit:
Once we have that assembled, we can run it with the
and it will give us a
Dynamic that collects the changing result values as the user interacts with the workflow.
An example will help.
We set up a “Switch button”:
and then we start creating pieces of the workflow.
The first piece will lay out the
textWidget on the page, and will transition to the second piece of the workflow when “Switch” is pressed:
The second piece will lay out the
tickWidget on the page, and will transition to the first piece of the workflow when “Switch” is pressed:
We don’t have to worry about rigging up a toggle
Event and keeping everything synchronized, we just set up the graph for the user to navigate.
We start the user on the first piece of the workflow:
which gives us a
Dynamic t (Event t Text), and we know what to do with that:
All in one place it looks like:
workflowWidget :: MonadWidget t m => m () workflowWidget = el "div" $ do eSwitch <- el "div" $ button "Switch" let -- Setup a piece of a workflow that wf1 :: Workflow t m (Event t Text) wf1 = Workflow $ do -- puts a `textWidget on the page` eText <- textWidget -- and moves to another piece of the workflow when "Switch" is pressed: pure (eText, wf2 <$ eSwitch) -- Setup a piece of a workflow that wf2 :: Workflow t m (Event t Text) wf2 = Workflow $ do -- puts a `tickWidget` on the page eText <- tickWidget -- and moves to another piece of the workflow when "Switch" is pressed: pure (eText, wf1 <$ eSwitch) -- Run the workflow and get a hold of the `Dynamic` that collects the results -- of the journey through the workflow deText <- workflow wf1 let eText = switchDyn deText dText <- holdDyn "" . leftmost $ [ eText , "" <$ eSwitch ] el "div"$ dynText dText
This gives us the same behavior as we had previously:
We can use this to switch between our various widgets in a cycle:
wf1 :: Workflow t m (Event t Text) wf1 = Workflow $ do eText <- textWidget pure (eText, wf2 <$ eSwitch) wf2 :: Workflow t m (Event t Text) wf2 = Workflow $ do eText <- buttonWidget pure (eText, wf3 <$ eSwitch) wf3 :: Workflow t m (Event t Text) wf3 = Workflow $ do eText <- tickWidget pure (eText, wf1 <$ eSwitch)
or we can give each widget it’s own “Next” and “Back” buttons to arrange them more like a traditional wizard:
wf1 :: Workflow t m (Event t Text) wf1 = Workflow $ do eText <- textWidget eNext <- el "div" $ button "Next" let eOut = leftmost [eText, "" <$ eNext] pure (eOut, wf2 <$ eNext) wf2 :: Workflow t m (Event t Text) wf2 = Workflow $ do eText <- buttonWidget eBack <- el "div" $ button "Back" eNext <- el "div" $ button "Next" let eOut = leftmost [eText, "" <$ eBack, "" <$ eNext] pure (eOut, leftmost [wf1 <$ eBack, wf3 <$ eNext]) wf3 :: Workflow t m (Event t Text) wf3 = Workflow $ do eText <- tickWidget eBack <- el "div" $ button "Back" let eOut = leftmost [eText, "" <$ eBack] pure (eOut, wf2 <$ eBack)
Playing along at home
If you want to test out your understanding of how switching works, there are exercises coming soon. 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.
In the next post we’ll look at how we break things up into components in
reflex, and the various design tradeoffs that are involved with that.