Over the last couple of months I’ve written 1,632 lines of purely reactive code in Sodium on the Times Tables Race Game for iOS using the GHC Haskell to iOS cross compiler. I know enough to express myself in the paradigm, and I know from experience that Sodium is not wanting in expressive power. (I have not had to “cheat” and do any part of the logic in an imperative/observer pattern style.) But this is the biggest chunk of reactive programming I have ever done in anger. It’s only really been possible to get stuck in now that I no longer have to develop Sodium at the same time.
If you are new to reactive programming as almost everyone is, there is a shift in understanding that’s required to think reactively. The following are the experiences of someone who has already gone through that and is now getting down to some real work.
(Note that you can load your own photos as backgrounds to the game!)
One thing I’ve found is that functions tend to have a lot of arguments and return values, typically 6 -> 3 for a checkBox widget and 9 -> 6 for a whole page (the settings page). This is mainly because reactive programming requires all inputs and outputs to be declared explicitly. There is a lot of scope for this to be abstracted away into such concepts as Widget, but in this simple game I have not done this.
The long argument lists have been a bit crufty at times, but we are in the early days of reactive programming. I expect the solutions to these niggles to become clearer in time.
I’ve found refactoring to be a dream, as with all functional programming. For example, having written the one player game, in the two-player game I needed to introduce a countdown 3.. 2.. 1.. between rounds. The eNextQuestion event was looping round and being fed back in to give the start of the next question. To add the countdown, all I needed to do was to pass the value out of the bottom of the function and eStartOfQuestion back in the top, so the looping of this value is done by the caller. Easy.
While the code can be a little long-winded if written directly in reactive primitives, the potential for abstraction is high, and it’s been easy to factor out common patterns and reuse code.
Factoring out these common patterns turns out to be quite important for keeping the code tidy. It can turn into spaghetti if the functions get too long. In fact, reactive programming is almost like designing a circuit, so it really is like spaghetti. Like the proverbial sea sponge in the blender, there is no order dependency whatsoever between lines of code. You could take any reactive function, and re-order the lines randomly and it would work exactly the same.
But when the code gets a bit spaghetti-like, it’s generally easy to fix. A lot of the refactoring has amounted to not much more than cut & paste. You can refactor with abandon, since the compiler is fussy it catches most mistakes, and the risk of breakage is very low.
The abstractions are often very neat, for example this code gives a value that increases over time from 0 to 1 triggered by a start event, as a building block for animating fades:
-- | Returns the fraction of the blend if it's running, and the
-- end-of-blend event.
blend :: Double -- ^ Duration of blend
-> Event () -- ^ Event to initiate the blend
-> Behavior Double -- ^ Clock
-> Reactive (Behavior (Maybe Float), Event ())
blend duration eInitiate time = do
blendStart <- hold Nothing $
snapshotWith (\() t -> Just t) eInitiate time `merge`
fmap (const Nothing) eEnd
let eEnd = filterJust $ snapshotWith (\t mT0 ->
case mT0 of
Just t0 | t - t0 >= duration -> Just ()
_ -> Nothing)
(changes time) blendStart
let blend = liftA2 (\mT0 t -> case mT0 of
Just t0 -> Just (realToFrac $ min 1 $ (t - t0) / duration)
Nothing -> Nothing) blendStart time
return (blend, eEnd)
“rec” declares a recursive block. All that does is allow for forward references (eEnd is referenced in the first statement), thus making loops possible.
The compiler catches most errors, and I’ve found the logic is easy to reason about. So much so, that things have tended to “just work” to a surprising extent. Interactions between different things in the logic tend to be more predictable, because the rules they follow are well-defined. For instance, if an event switches one thing off (e.g. a fade out) and another thing on (a fade in), then when combining the two later, you never have to worry that in some context there might be a detectable gap between them, because that is impossible.
Debugging has been easy enough with the occasional need to add trace messages to see what certain values are at runtime.
I’ve been very pleased with performance, which I was a little concerned about. Mobile phone processors are not fast compared to what we’re used to with PCs.
It’s been a long time since I would have attempted something like this in an imperative/observer pattern style, so it’s difficult for me to compare how long things take. Certainly there have been some areas that I breezed through that were complex enough that have given me fair anxiety in an imperative/observer context, such as all the complexities of image loading, evicting unused images from memory, and OpenGL generally. (These aspects were technically outside the reactive logic but ultimately controlled and shaped by it.)
Ultimately the goal of reactive programming is to deal with complexity on a very large scale. I believe that reactive programming does this, but it’s not until I’ve written 16,320 or 163,200 lines when that will prove itself, but my intuition is that it will.