What The Hell Is Functional Reactive Programming?
We’re really excited about Fairmont, our open source library for functional reactive programming. But in our enthusiasm to show you what Fairmont can do, we haven’t spent much time talking about FRP.
Namely, what the hell is FRP and why should you care? Image credit: Mark Finn.
The What
Reactive programming is a close cousin to event-driven programming. The basic idea is that we’re responding to an event. An event could be anything from a button click to a response to a network request from a remote server. In a sense, any time you do these things, you’re practicing a form of reactive programming.
The Why
The problem is that we have n events and m values affected by those events. We can refer to the latter as mutations. The number of possible interactions between events and mutations is n x m. That is, it grows combinatorially with the numbers of events and mutations. For non-trivial applications, this leads to complexity, which leads to brittleness. Changes become increasingly difficult to make, potentially affecting n x m places in our code.
Reactive programming, per se, is about decoupling each combination of event and mutation so that we can add them, change them, or even remove them without affecting the rest of our code.
Example: A Simple Counter
Let’s consider a simple example: a counter. We have buttons to increment and decrement our counter. And we must display the current value of the counter. We can write this code with just two event handlers.
counter = 0
incrementButton.click ->
counter++
counterText.text counter
decrementButton.click ->
counter--
counterText.text counter
We appear to have two event-mutation combinations. When the increment button is clicked (event) we increment the counter (mutation). And when the decrement button is clicked, we decrement the counter. But there is a third concealed within them, namely, when the counter is updated, we update the display. Consequently, we duplicate the code to update the display.
Complexity Grows Combinatorially
Let’s add a way to reset the counter to zero.
resetButton.click ->
counter = 0
counterText.text counter
The danger is growing. Any change to the way we update the counter now affects three different event handlers.
We get a new requirement to provide an input field to set the counter. Easy enough, right?
counterInput.change ->
counter = counterInput.val()
counterText.text counter
But right away, we see a problem. The input field is not updated when we click the increment or decrement buttons. We need to update the input field along with the text. This affects code in four different event handlers.
Good: Reduce Coupling
Naturally, we define a function, updateCounter
to encapsulate the logic for updating the counter.
This is a good plan.
This isolates changes to the update logic to that single function.
On the other hand, we now have to remember to call
the updateCounter
function any time we modify the counter.
Six months from now, another developer may come in and add a slider control.
We need to make sure they know to use our function
rather than updating the input field and text directly.
Better: Eliminate Coupling
So we decide to employ the Observer pattern. We’ll observe the counter value and update the display when it changes. This frees our event handlers from worrying about how to update the display.
value = counter: 0
Object.observe value, ->
counterInput.val value.counter
counterText.text value.counter
incrementButton.click ->
value.counter++
decrementButton.click ->
value.counter--
resetButton.click ->
value.counter = 0
If another developer adds that slider control six months from now, all they need to do is change the value object. The observer will update the display automatically.
Mutations As Events
We’re treating mutations as events. So we can event split these out if we want to fully decouple events and mutations.
Object.observe value, ->
counterInput.val value.counter
Object.observe value, ->
counterText.text value.counter
We can now add, change, or remove display elements independent of the rest of code. Let’s add a logger.
Object.observe value, (changes) ->
console.log changes
We don’t know or care about the other event handlers in implementing this one. We can incorporate additional complexity without introducing coupling between events and mutations. Our hypothetical future developer can add that slider without thinking or knowing much about how the rest of the application works.
Uniform Interfaces
We’ve managed all this with only JQuery and the ES6 observer interface. That’s because we have two kinds of events: button clicks and mutations to value objects. But each time we add a new kind of event, we’ll probably end up with a different interface to handle it.
For example, server-sent events use an onmessage
handler.
This introduces another subtle source of coupling.
If we change the event source,
we probably need to change the code handling it.
But even if we don’t, we must familiarize ourselves with all these different interfaces. And so does our hypothetical slider implementor. And anyone else who works on our application.
Reactive programming libraries provide a uniform interface for dealing with events. Each source must provide an adapter that conforms to this interface. This way, button click events and observer events look the same. Developers working on our application need only be familiar with one event interface.
Good: Event-Oriented
But what would make the best interface?
We might consider something like onEvent
:
value = counter: 0
onEvent 'click', incrementButton, ->
value.counter++
onEvent 'click', decrementButton, ->
value.counter--
onEvent 'click', resetButton, ->
value.counter = 0
onEvent 'change', value, ->
counterInput.val value.counter
onEvent 'change', value, ->
counterText.text value.counter
onEvent 'change', value, ->
console.log changes
This is nice. We handle click events the same way we handle mutation events. We still need to write adapter code for each event source, but those details no longer concern our application code.
A new requirement comes in. We need to limit the range of the counter to values from 0 to 99. Easy, right?
value = counter: 0
onEvent 'click', incrementButton, ->
value.counter++ if value.counter < 99
onEvent 'click', decrementButton, ->
value.counter-- if value.counter > 0
Of course, we’ve just reintroduced the entire problem we were originally trying to solve. All the code that updates the counter has to know about this new range requirement. We can instead add a mutation handler.
onEvent 'change', value, ->
if value.counter > 99
value.counter = 99
if value.counter < 0
value.counter = 0
But this will generate two change events when we reach our boundary conditions, when we would prefer to not to generate any. We need a way to filter the change events so that they don’t fire if the value is out of range.
Better: Stream-Oriented Interface
This suggests that a better interface for reactive programming
is collections of events, or event streams.
We can implement collection operations, like filter
,
that operate on event streams.
These would produce new event streams,
much the way filter
on an array returns another array.
We could then use the filtered event stream for display updates.
One problem is that writing this code out is tedious:
unfilteredCounterChanges = eventStream 'change', value
rangeFilter = ({counter}) -> 0 <= counter <= 99
counterChanges = filter rangeFilter, unfilteredCounterChanges
each counterChanges, ->
counterInput.val value.counter
each counterChanges, ->
counterText.text value.counter
each counterChanges, ->
console.log changes
However, through the use of currying and composition we can simplify this considerably. Here’s how our little application would look using Fairmont:
value = counter: 0
# use value.changes as the change event stream,
# rather than creating one directly
value.updates = flow [
events 'change', observe value
filter ({counter}) -> 0 <= counter <= 99
]
go [
events 'click', incrementButton
tee -> value.counter++
]
go [
events 'click', decrementButton
tee -> value.counter--
]
go [
events 'click', resetButton
tee -> value.counter = 0
]
go [
value.updates
tee -> counterInput.val value.counter
]
go [
value.updates
tee -> counterText.text value.counter
]
go [
value.updates
tee -> counterText.text value.counter
]
(For our purposes here,
you can think of tee
function as being similar to map
.)
We could nitpick that our future slider developer
needs to know to use value.changes
instead of creating a new event stream.
But we can defend that as a part of the interface
to our value
object.
It’s no different than knowing that the value.counter
is the current count.
And we have six separate pairs of events and mutations. Each of which is independent from others. We can add new ones without affecting them.
FRP FTW
We can characterize reactive programming by these techniques:
-
Writing code in terms of discrete events and mutations.
-
Modeling mutations themselves as events.
-
Operating on event streams instead of individual events.
Fairmont builds on this approach by making use of constructs from the world of functional programming, including both currying and composition. That brings us into the realm of functional reactive programming. Fairmont also uses asynchronous iterators (called reactors), to implement event streams.
Functional reactive programming makes it possible to deal with the combinatoric growth in program complexity arising from adding more events and mutations. Real-world applications have dozens of each, resulting in thousands of potential interactions. Being able to model each of these independent of one another is a big win. The more sophisticated the application, the bigger the win.