Skip to content

Instantly share code, notes, and snippets.

@lhorie
Last active February 15, 2022 03:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lhorie/da1cfad2a4c12ff36256f2da0a04c33b to your computer and use it in GitHub Desktop.
Save lhorie/da1cfad2a4c12ff36256f2da0a04c33b to your computer and use it in GitHub Desktop.
Dear Javascript developer, OOP and FP are not opposites

Dear Javascript developer, OOP and FP are not opposites

In the Javascript community, I sometimes hear people say they prefer FP over OOP, and that FP is good and OOP is bad. The line of argument implies there's an FP vs OOP dichotomy, and that one should aim to use one and avoid the other.

I like to quote the Qc-Na parable because it illustrates a subtle but important insight about the two paradigms: they are not opposites and in fact have a lot in common. Here's the parable for those who haven't seen it:

The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

So what's this enlightenment all about anyways?

Javascript old timers will immediately spot something familiar in that tale: using closures to implement object oriented patterns is something we were doing for ages before ES6 came along:

function greeting() {
  var word = 'hello';
  return {
    set: function(w) {
      word = w;
    },
    get: function() {
      return word;
    }
  };
}
var myGreeting = greeting();

In a nutshell, we can implement one paradigm in terms of the other, so in essence they're not that fundamentally different after all. The example above is in fact strictly functional but it suffers from the same design problems that are often associated with bad OOP: arbitrarily mutable state and procedural APIs.

N.B: recall that purity is not a hard requirement for FP as a whole, only for a subset of it. Many JS libraries abstract over impurity via functional patterns. The implementation of React's setState hook in its functional components, for example, isn't far off from the snippet above.

Now, notice that the following snippet also suffers from a similar mutability-from-who-knows-where problem:

let mutableGreeting = 'hello'; // greeting is mutable

And so does this other very familiar-looking pattern, for the exact same reason:

const greetingObject = {greeting: 'hello'}; // greeting is mutable

Mechanically speaking, the only difference between the myGreeting and greetingObject is in how one uses the value idiomatically. Calling myGreeting.set(value) is supported by design but not air quotes "purely functional", and the same goes for greetingObject.greeting = value. This is why sometimes you might hear people say that Javascript doesn't have "true" functional programming, only functional style. It means impure escape hatches will almost always physically exist, but stylistically, they are considered non-idiomatic.

What these contrived examples suggest is that rather than disliking some intrinsic property of OOP, the thing that we actually consider undesirable is arbitrary mutations. We don't want to look at an object somewhere in our code and wonder where a mutation in it came from, regardless of whether the object was instantiated from a class or passed through a functional pipeline or is merely in a variable somewhere.

Inheritance as a root of evil

Detractors of OOP will often say that OOP is primarily about inheritance and that inheritance is bad. But is it really?

Let's look at an example that embodies what is typically called "functional" among Javascript circles, the _.chain example from lodash:

const users = [
  { 'user': 'barney',  'age': 36 },
  { 'user': 'fred',    'age': 40 },
  { 'user': 'pebbles', 'age': 1 },
];
 
const youngest = _
  .chain(users)
  .sortBy('age')
  .map(o => o.user + ' is ' + o.age)
  .head()
  .value();

In a nutshell, we are passing a list of objects through a pipeline of operations and coming out with a value at the end. Functional, right? Well actually, there's also object orientation going on: all those preceding dots are an usage of fluent interfaces, an OOP design pattern.

Let's first compare it with a more functional implementation to see what a variation would look like without relying on OOP for composition (note, I'm ignoring edge cases for the sake of simplicity):

const sortBy = key => array => array.sort((a, b) => a[key] - b[key]);
const map = fn => array => array.map(fn);
const head = array => array[0];
const chain = value => ([head, ...rest]) => head ? chain(head(value))(rest) : value;

const users = [
  { 'user': 'barney',  'age': 36 },
  { 'user': 'fred',    'age': 40 },
  { 'user': 'pebbles', 'age': 1 },
];

const youngest = chain(users)([
  sortBy('age'),
  map(o => o.user + ' is ' + o.age),
  head,
]);

We can see that the code to populate the youngest variable looks relatively similar structure-wise, aside from the slightly strange looking currying.

Now let's look at a more OOP-centric implementation, using "big bad" inheritance to boot:

class A extends Array {
  sortBy(key) { return this.sort((a, b) => a[key] - b[key]); }
  head() { return this[0]; }
};

const users = [
  { 'user': 'barney',  'age': 36 },
  { 'user': 'fred',    'age': 40 },
  { 'user': 'pebbles', 'age': 1 },
];

const youngest = A.from(users)
  .sortBy('age')
  .map(o => o.user + ' is ' + o.age)
  .head()

This looks quite a bit closer syntax-wise to the lodash example (being also based on fluent interfaces), except that it doesn't require remembering to unwrap .value() at the end.

Now be honest. Between the more functional flavor and the more object oriented flavor, which one would you rather maintain/debug and why? There's a good chance that you find the OOP version easier to grok, because fluent interfaces are familiar to programmers trained in C-derived languages, and techniques like currying are not.

There's something called a honeymoon phase, which is common during the early stages of learning something. Many JS developers are still in a stage where their biggest exposure to functional concepts comes from the bliss of learning to switch from for loops to Array.prototype.map and friends, but they have yet to see the true depths of the FP abyss.

The functional snippet above may already seem somewhat daunting to the less advanced developers, even though it only uses basic functional patterns, and yet it's merely a glimpse of what lurks beneath. For example, I didn't mention that there's a nasty obscure memory bug in that code. Would your love for FP still show after getting battle scars from tackling issues like it? Or is the comfort of OOP fluent-interface-based composition more homely after all?

Stronger together

We have a tendency to want to pick teams in our quest for programming mastery, but we can see from lodash's mix-and-match approach that as far as FP vs OOP goes, the two paradigms are not really at odds with each other in practice (and can in fact complement each other well).

A good takeaway from this exploration is that rather than antagonizing OOP, it may make more sense to embrace it along with FP, but stick to the familiar subsets of each paradigm to achieve our goals.

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