Using a tool that is new to us doesn’t automatically make our brains adopt a corresponding way of thinking. Thus, opportunities for misuse and overcomplication present themselves in many places.
In this case the new tool is Elm, and the way of thinking is Elm’s particular interpretation of FRP. Specifically signals. We’re going to see that having a seemingly convenient signal plugged to the wrong part of our program can lead us to making up problems, and having to come up with convoluted ways of solving them.
Input signals and the “Update” layer
While built-in goodies such as
Mouse.isDown are pretty much
the cornerstone of our Elm GUIs, we can (and sometimes should, as we’ll see) control,
transform, and interpret these primitive signals, to create more semantic ones.
This is not just a matter of aliasing the
False values of
more meaningful ones, i.e.
Idle. It is also a matter of creating the
simplest and most correct signal for the job.
What I’m going to do here is stage a common situation I’ve found myself and others (from the Elm questions I’ve gotten from other newcomers) in.
Common example: Adding a pause button to a program
Consider the following program.
Typical organization of an Elm program. Explicit, unambiguous separation of concerns into input, model, update, and render modules (well, “sections” of a single file, for these particular examples).
The idea is to be able to pause the program with the keyboard. So let’s add
another signal to the
The type – which isn’t annotated because type inference FTW – of our
input value is now
That is, a signal of a pair of values: the time delta, and a boolean representing whether the
enter key is down.
The first argument of the
update function has also been modified to accept the new shape of our input.
So far so good, but we want to toggle the state, which means we can’t just change the
update code to:
Right? Because this “pause” functionality would pause the program only while the enter key is down. When we release the button, the program will resume, and that’s not the behavior we want.
Therefore we’re going to need more than just the
Adding a representation of the program’s state
Presumably, we want some representation of the running state of the program.
Once we have that as a value, the plan is then to check
update function, and if it’s
True, toggle the value which
actually represents the state of the program.
Before proceeding: Do you see a problem with this plan?.
Let’s add an algebraic data type for representing the state, and attach an initial value of this type to our model.
This doesn’t change the behavior of our program. We’ve just added more data to work with.
Now let’s modify the
update function according to plan.
The program now toggles the state depending on what the current value of
and then the meat of our update logic acts according to the computed state.
However, if we tested this program, we’d see that our pause button is broken: Sometimes it pauses the program. Sometimes it doesn’t. Sometimes it pauses and then immediately resumes.
Our plan has a problem
The plan seemed complete: If
True, toggle the
ProgramState. What else
is there to do?.
The problem is that we forgot about the fact that the
update function is executed 24 times per second!.
From the moment we press down the enter key, until the moment we release it,
update will have seen
True (and therefore toggled the state) a bunch of times.
If you already know a bit about Elm, you’re probably thinking about a signal-filtering
function such as
dropRepeats. But that wouldn’t solve the problem,
because even if you plugged
dropRepeats in front of
function would still see the current value of the signal (repeated or not) 24 times
per second. (However, you are on the right track!)
Further action is needed
This is where things will get convoluted. However, since there’s people who browse the web for tutorials and then copy/paste code without even reading the actual explanations for it, I will not write the actual code for the convoluted solutions to our current problem, just in case!.
But it’s easy to imagine that the
“next step” in the evolution of our increasingly convoluted program would be
to add more state and conditions to the update logic to basically, somehow,
toggle the state if
Keyboard.enter is down, unless it’s just been
pressed and it hasn’t been released. Or something like that.
To be clear: we could (and I have, and seen others do, too) keep adding more flags and state and crap to our current program, and eventually get a functioning pause button (or similar requirement), but we should stop here, and take a couple of steps back.
Where did we go wrong?
At the beginning, actually.
Remember I mentioned the idea of a convenient signal connected to the wrong part of our program.
If we go back to our introduction of the
Keyboard.enter signal, we’ll see that the
function changed to:
But I did not ask the question: is
Keyboard.enter plugged to
update a sensible signal
path for our ultimate goal?. And the answer to that question is negative.
I made the following logical leap (which perhaps you caught right away): “Program P needs to react to key K. Therefore, update logic U needs to react to key K.”
The worst thing about this leap is that, after it was made, all further problems and
“solutions” appeared to be the concern of the Update layer. So, as I saw that the program
still wouldn’t behave as expected, I kept wanting to add more logic to the
Designing our signals
As I’ve hinted enough already, what we need to do is a bit of signal transformation.
ProgramState algebraic type and its
toggleState operation were actually a good idea,
so let’s keep ‘em, but use them better:
And there we have it!.
Look at the
update function. It no longer reacts to the current value of
Keyboard.enter. Rather, it reacts to the current value of a
Does it matter that the
update function gets executed multiple times while our finger is
enter? Nope! As can be seen in the definition of
input, the logic for toggling
ProgramState signal only runs whenever
Keyboard.enter updates discretely.
Now what the
update function sees 24 times per second – it will still run that many
times, since it has to react to the
(fps 24) signal we’re also plugging into it – is
whatever the current value of the
ProgramState signal is.
Additionally, note that we don’t even need to pollute our model
m with a
A last bit of refactoring
For extra points, let’s remove some repetition, by making a function that returns its first argument if a state is
Running, or the second argument otherwise. We can then use that function both for toggling the state in our
now more tailored
input, and for reacting to it within the
Motherflipper is done.
Revision Jan 10, 2014
…Or is it?. As Apanatshka has noted,
there is still one superfluous call to the
ifRunning function. And from what we’ve been talking about, it’s obvious which one that is.
But I’ll leave the actual further (and final) complete refactoring step as an exercise to the reader! (Hints in Apanatshka’s comment.)
We went back to the drawing board, and:
- Put the logic for computing the
ProgramStatein a better part of the program.
- Avoided a set of problems entirely, by recognizing the usefulness of Elm’s discrete signals and signal manipulation.
- Made the
updatefunction react to a more semantic signal of
ProgramState. Now it knows nothing about
Basically, we gained expressiveness and controlled complexity as a direct result of plugging the right signals into the right places.
Finally, a generalization of the lesson
(Because no programming blog post is complete without some goddamn “rule of thumb” by the author!)
Leaving signals, FRP, Elm, and actually pretty much all of the specifics of this post aside, there seems to be an additional, and more general lesson here, which if I had to formulate, would go something like this: The less interpretation logic – such as conditionals and other forms of probing – we do on the input arguments of a function, the more the function can be read as a simple equation, which is A Good Thing.
That’s all for now.