By @dmvaldman
Functional Reactive Programming (FRP) is generating buzz as an alternative to Object Oriented Programming (OOP) for certain use cases. However, an internet search quickly leads a curious and optimistic reader into the rabbit-hole of monads, functors, and other technical jargon. I’ve since emerged from this dark and lonely place with the realization that these words are mere implementation details, and that the core concepts are far more universal. In fact, the groundwork was laid down many centuries before the first computer, and has more to do with interpretations of reality, than structuring programs. Allow me to explain.
There’s an old thought experiment that goes like this:
###If a tree falls in the forest, and no one is there to hear it, does it make a sound?
There are a lot of ways to attack this question as ill-posed, but regardless it is trying to say something about our understanding of reality, specifically causality. Is observation an effect of existence, or the cause? Pretty heavy stuff. It turns out it helps to think of this in code. Let’s make a tree fall.
class Tree {
constructor(){
this._fell = false;
}
set fell(state){
this._fell = state;
}
get fell(){
return this._fell;
}
}
var tree = new Tree();
tree.fell = true;
Above is an object-oriented approach. Its patterns are getters, setters, and state. We have a Tree
object, and to make it fall we set its fallen state to true
. Seems straightforward enough, but here’s where things get interesting. If one were to question whether the tree fell even if they didn’t directly observe it, the answer is a resounding yes; anyone can query the tree after the fact by calling tree.fell
and decide that the tree has, indeed, fallen. Those that answer yes to our philosophical question typically do so on the grounds that one can later arrive at a forest, see the fallen tree and deduce that it fell some time in the past. This is the codified equivalent. Our search is over; we have our answer!
Not so fast reader. Let’s look at another approach.
class Tree extends EventEmitter {}
var tree = new Tree();
tree.emit("fall");
This is the functional-reactive approach. Its patterns are events, maps and in its purest form has no state. We create an instance of a Tree
object and make it fall by broadcasting a fall
event. But there is nothing listening! Our event falls on deaf ears. Anyone returning to this tree after-the-fact has no idea whether the tree fell or not, not to mention whether it made a sound. Indeed, nothing in our program has changed. Looks like we have a different answer!
###Descartes and Berkeley
The object-oriented and reactive approaches give two different answers to our thought experiment. This is because they embody two contradictory philosophies of epistemology: Rationalism, popularized by Descartes in the late 1600s, and Empiricism popularized by Berkeley in the early 1700s.
Descartes, in a streak of fanatical skepticism, found he could only be sure of one thing: his own existence. He came to this conclusion because he couldn’t doubt the existence of his thoughts and concluded there must be some entity doing the thinking, thus coining the phrase, cogito ergo sum: I think therefore I am.
But what is thought, other than changes to one’s internal state? The tree falls because its internal state went from false
to true
. Descartes is the quintessential object-oriented programmer.
Soon after Descartes, comes George Berkeley. Berkeley denounced the Rationalist view. To Berkeley, it made no sense for material objects, like trees, to have existence. The only existence comes to us in terms of thoughts (the mental), and thoughts must be assimilated in the mind to exist. Material objects are deceptions; their essence is not their physicality but their ability to transform the immaterial. If a thought is not assimilated into consciousness, it has no existence. Thus he popularized the Latin phrase esse percepi: to be is to be perceived. Berkeley is the quintessential Functional Reactive programmer.
To complete the analogy, though, I’ll need to expand the code example above. Something needs to happen in our application. The immaterial must make an impression in the mind.
class Air extends EventEmitter {
constructor (){
function mapFall (fall){...}
this.on('fall', function(fall){
var vibration = mapFall(fall);
this.emit('vibrate', vibration);
}.bind(this);
}
}
var air = new Air();
air.subscribe(tree);
Now we have something listening to the tree: an Air
object. When the tree falls, the air vibrates. But nothing is listening to the air! Still nothing has happened. We forge onward.
class Ear extends EventEmitter {
constructor (){
function mapFrequency (frequency){...}
this.on('vibrate', (frequency) => {
var electricalSignal = mapFrequency(frequency);
this.emit('signal', electricalSignal);
};
}
}
class Brain extends EventEmitter {
constructor (){
function mapSignal (signal){...}
this.on('signal', (signal) => {
var sound = mapSignal(signal);
this.emit('sound', sound);
};
}
}
We have effectively set up a chain of causality, ending in the brain which can assimilate the original event. We make this explicit by building a subscription pipeline:
brain.subscribe(ear).subscribe(air).subscribe(tree);
brain.on('sound', function(sound){
console.log(sound); // we exit the system. You have been heard!
});
tree.emit('fall', fallData);
In pictures it looks like this:
The arrows go from right to left because an effect subscribes to its cause. Events then propogate in the reverse direction, so the logic flows from left to right.
Here we see an interesting fact: if there is no consciousness present, or if the connection between the brain
and the ear
is severed, then the mapSignal
function will never be called and no sound will be processed, even if every other step of the pipeline remains intact.
Berkeley called this concept Subjective Idealism. Idealism because it asserts only thoughts or ideas exist, and subjective because reality is dependent on the subjects that perceive it. In my humble opinion, Subjective Idealism is the philosophy underpinning reactive programming. Berkeley writes,
It is indeed an opinion strangely prevailing amongst men, that houses, mountains, rivers, and in a word all sensible objects have an existence natural or real, distinct from their being perceived by the understanding. ...For what are the forementioned objects but the things we perceive by sense...and is it not plainly repugnant that any one of these or any combination of them should exist unperceived?
I love this quote for its self-assured audacity. Berkeley is essentially calling all of us crazy for thinking that houses, mountains and rivers exist. In our example, trees, air, ears and brains are false idols; the only reality is mapFall
, mapFrequency
and mapSignal
and the resulting assimilation in the mind. Material objects are nothing more than transformations of ideas.
###Implementation and Design
I've been dwelling on the philosophical aspects, and less about the implementation of reactive programming. FRP is about building pipelines and sending events with data through them. You can do fancy things like filter and merge events, etc. The business logic of applications is no longer about synchronizing state, but about transforming events: the Berkelian reality. Many applications are easier to reason about this way, especially if their business logic can be thought of in a pipeline metaphor. For a great technical overview, see this writeup.
But this is only how FRP is implemented in applications. There is another feature of FRP which is seldom discussed but is intrinsic to its design pattern. FRP begins with what a user of an application observes and then the business logic (the subscription of various EventEmitter
instances) is set up backwards. A related concept can be found in computer graphics with ray tracing. To draw the screen, you start from the pixels and go backwards to figure out what light sources affect them. This is opposite to the physics, which starts from the light sources and evolves forwards until the light hits the screen. The ray tracing approach is far more performant, as you never waste computation on a light ray that strikes off-screen. This is fundamental to FRP: no computation is wasted on changes that are not observed. You get performance for free.
###Time and Space
A person is only perceiving what is in their field of view at the present moment in time. Duh, right? But by limiting an application to only be concerned with the present and the observable is not a common idea. Ray tracing is a spacial example, lazy evaluation is another. You're familiar with a temporal example if you've ever done dirty checking, cache invalidation, diffing etc. These occur when there is a conflict between the present and the past. State, in an application is typically a snapshot of the past, waiting to be updated, only to recede into the irrelevancy of the past once more. State is residual inventory, and the cause of much pain to developers trying to keep it synchronized. If your application can be thought of as a pipeline, there is no need to hold on to the past, and state can be eliminated. It is only when you don't know when two parts of an application will interact that you need to hold on to their state (e.g., collisions in a game).
In FRP, the cause and effect pipeline is one of tell, don't ask. What I mean by this is that parent components don't ask, or query, their child components to see if they've changed. Rather, child components tell their parent components to update when they have (the ear tells the brain). Parents are passively listening, which is why the arrows in the pictoral diagram above seem to point backwards to the logic flow. As data is updated it overwrites the past without asking for permission, just like real life! Nature doesn't diff. Nature evolves over time. It doesn't check to see whether the past is still valid.
So far these concepts are rather vague and meant to be agnostic to any particular application. In the next post, I'll discuss how to build a web application framework using these principles and we'll get into the nitty gritty. If you want updates, follow me on Twitter @dmvaldman. You can also see the library I'm building that embraces these ideas at samsaraJS.org.
Anyway, a question. If esse is percipi, where do changes come from?