What The Hell Is Functional Reactive Programming?

Tony Stark used reactive programming to power his Iron Man suit. Or something like that.

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:

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.

Notes