The case for Functional Reactive Programming
This post is a summary of a talk I gave today, in which I made the case for Functional Reactive Programming in Javascript using libraries such as RxJS.
Bugs, bugs, bugs
If debugging is the process of removing bugs, then programming must be the process of putting them in. —Edsger W. Dijkstra
All else being equal, software with fewer bugs is better than software with more bugs. To minimise the amount of bugs we ship, we spend a considerable amount of effort on debugging. A more time-efficient way of achieving this same goal, of course, is to prevent us from writing these bugs in the first place.
To find out how we can avoid writing bugs, let's consider what a bug is: software doing things the programmer did not expect it to do. In other words: the harder it is for us to understand the code we work on, the more likely it is to contain bugs.
So what makes our code hard to follow? Consider the following example code:
let counter = 0; function increment(){ counter++; } function decrement(){ counter--; }
It looks rather innocent: we have a counter with an initial value of 0, and the functions increment
and decrement
to respectively increase and decrease its value by 1. It exhibits a problematic property described by three words: shared mutable state.
In short, the state of the application is located in counter
, which holds the current value of the counter at any point during the lifetime of the application. Both functions share access to counter
, and both can modify ("mutate") its value.
Looking at the code above, my earlier claim that this is hard to follow seems absurd. Consider, however, any non-trivial codebase, where functions consist of more than one line, and increment
, decrement
and counter
might all be located in different files. In that case, anyone who wants to edit either of those will have to keep the rest of the codebase in mind, because bugs might be lurking there when you modify the code here.
Our saviour: pure functions
The way around this is by using pure functions: functions that do not modify anything outside of themselves (in other words: they don't rely on side effects). Instead, they receive everything they need as input, apply some transformations, and provide the result as output. With this in mind, the increment
function might receive a number representing the current counter value, and return a new number that is 1 higher than that:
function increment(counter){ return counter + 1; }
And of course, likewise for decrement
.
So how would we use functions written in this way? Well, rather than simply calling the function and hoping for something to happen, we have to pass it the correct input and then use the output to update the counter:
let counter = 0; counter = increment(counter);
But of course, this is not what we usually do. Rather, we increment the counter in response to e.g. the user clicking a button. So we'd actually write a handler for the click event:
let counter = 0; function onClick(){ counter = increment(counter); }
…but now we're back in the land of shared mutable state: onClick
changes a value outside of its own scope! Does this mean we're stuck now? Of course not!
Functional Reactive Programming to the rescue!
Using the principles of Functional Reactive Programming, we can write the largest part of our application using nothing but pure functions. We can do so by pushing side effects to the "edge" of our application, where it interacts with the "outside world", such as responding user input or receiving HTTP responses. We convert those into Observables as soon as possible, which we can manipulate using nothing but pure functions.
For our counter example, this would look somewhat like this:
// Convert user input to Observables const incrementButton = document.getElementById('incrementButton'); const decrementButton = document.getElementById('decrementButton'); const incrementClick$ = Observable.fromEvent(incrementButton, 'click'); const decrementClick$ = Observable.fromEvent(decrementButton, 'click'); // Convert this input to an Observable producing counter values, // using nothing but pure functions const counter$ = Observable.merge( incrementClick$.map(() => 1), decrementClick$.map(() => -1) ) .startWith(0) .scan((accumulator, value) => accumulator + value); // Subscribe to counter values, and display them to the user counter$.subscribe(counter => { document.getElementById('counter').innerHTML = counter.toString(); });
As you can see, we need a few lines of side effects at the top and bottom where we deal with the outside world. The meat of our application, however, consists only of pure functions that do not share state. The challenge here is to understand Observables, but once you've got that down FRP code is relatively easy to follow. Which means we'll write fewer bugs!
(And that's even without considering how easy it is to write unit tests for pure functions.)
Summing up
As we saw, code that is hard to follow has more bugs. Code becomes hard to follow when it contains shared mutable state. Pure functions avoid this, but it can be a challenge to use them extensively.
That is where Functional Reactive Programming comes in. FRP allows us to represent changing state, while allowing us to keep (most of) our application logic pure.
If this captured your interest, I would encourage you to read more about Functional Reactive Programming.
This work by Vincent Tunru is licensed under a Creative Commons Attribution 4.0 International License.