Skip to content

Instantly share code, notes, and snippets.

@dmvaldman
Last active February 23, 2024 16:24
Show Gist options
  • Save dmvaldman/f957dd9a8ed3f6edf35d to your computer and use it in GitHub Desktop.
Save dmvaldman/f957dd9a8ed3f6edf35d to your computer and use it in GitHub Desktop.
Descartes, Berkeley and Functional Reactive Programming

Descartes, Berkeley and Functional Reactive Programming

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:

Tree

###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

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.

Berkeley

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:

material subscription

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.

@bwo
Copy link

bwo commented Aug 25, 2015

Anyway, a question. If esse is percipi, where do changes come from?

@dmvaldman
Copy link
Author

@bwo a very good question indeed! I imagine it's one big feedback system. But as to the original change(s), no idea :-)

@kragen
Copy link

kragen commented Sep 7, 2015

Mirroring my lobste.rs comment because I feel that I have an ethical obligation not to snipe at people behind their backs:

Having more or less read this article, I can now agree with @ddellacosta that the article was written by somebody who doesn’t know what FRP is; instead, they have confused FRP with a particular style of asynchronous event notification which could be used to implement either FRP or non-FRP systems, and which FRP systems can be implemented with or without. It is the programming equivalent of explaining that an “automobile” is a piece of steel with unusually high vanadium content.

@dmvaldman
Copy link
Author

@kragen Oh, thanks for pointing out the discussion on that forum. I hadn't seen it before. I tried to leave a comment there but I need to be invited in order to post. Do you think you could invite me? It would be great to centralize the discussion somehow. I'd prefer to do it here on this gist, if you could point that out to those commenting on the forum, that would be great.

To your comment, it's true that I've focused on the asynchronous event notification part in this post. There is no talk about core FRP ideas of continuous time, illusion of simultaneity, etc. These are necessary to mention to gives justice to FRP design, but they are higher level constructs, and indeed implemented with message passing. My post is not meant to replace the efforts of others to explain FRP, but to compliment them by taking a somewhat different approach. I don't believe that someone can walk away from this post with a full understanding of FRP, but that was never the intention. This post was meant as a starting point to tackle further concepts.

@JaekwanLee
Copy link

Hm... so simply speaking, these two programming are based out of focusing on either objectivity or subjectivity. On the one side, functional programmers may believe that subjectivity is more stable and reliable in terms of volunabiltiy, aka system failures. However, in my opinion, oop programmers may argue the idea of using subjectivity in terms of correctness. The system may not fail but the result of system may not be correct. In human life, we often see the subjectivity failed. So I have two questions for you. First, do you think that the FRP is still good in terms of correctness? Second, if you agree, what do you think which one is more important between volunability and correctness. If you dont agree, why?

@dmvaldman
Copy link
Author

I've rewritten and expanded this post and republished it as a two-parter here: https://medium.com/@dmvaldman/descartes-berkeley-and-functional-reactive-programming-18b0b61eac58

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment