Functional programming and immutable data structures in Javascript

Bertrand Junqua
5 min readMar 11, 2018

--

A few months ago, I stumbled upon this conference by Anjana Vakil, explaining immutable data structures and pointing to two libraries enabling us to use those in Javascript.
The first one was the very famous Immutable.js by Facebook, and the second one was Mori, a port of ClojureScript’s data structures, that had better performance and a 100% functional API.

A few months later, I found myself having to refactor a very CPU-heavy Node.js service handling huge amounts of data and transforming them in various ways.

This is when I remembered Anjana Vakil’s conference and decided to dig further into using immutable data structures. My first step was to experiment with a JSPerf test:

Native JS array filtering VS filtering on a mori list

As you can see, the results were quite spectacular.

The above test is comparing using JS’ filter method on a native array against using Mori’s filter function on a Mori list. While the former re-creates an array from scratch with the values satisfying the isAboveFifty predicate, the latter leverages immutable data structures to create a new list referencing the old one, only different in that it discarded the values we don’t want to keep.

Rejoiced by those results, I decided to start using Mori to refactor my Node.js service. And this is where it gets interesting, because I had been blending in functional programming principles in my daily JS code for some time now, but Mori actually enabled me (or should I say forced me) to refactor all of the data manipulation code to adopt a strict FP style.

Consider the following snippet of code:

const rows = [
{ a: 'foo', b: 'bar' },
{ a: 'baz', b: 'qux' },
];
const processRows = (rows, flag) => {
if (flag) {
rows.forEach((row) => {
row.a = 'test';
}
}
... more processing
};
processRows(rows, true);

Now, while this works, it has the disadvantage of mutating directly the input data, and that comes with many risks. So let’s try and avoid that by refactoring this function in a slightly more functional style:

const processRows = (rows, flag) => {
let newRows = rows;
if (flag) {
newsRows = rows.map(row => ({ ...row, a: 'test' }));
}
... more processing
return newRows;
};
const result = processRows(rows, true);

Now we are not mutating the input data anymore, but we got ourselves a couple of issues: first of all, we’re assigning and re-assigning a variable, which is not something you want to do when writing functional code (plus it’s ugly), but most importantly we are re-creating a full array with all of our rows on every map, and that is wasting a lot of resources.

Let’s start with the first issue, and extract that conditional processing to a separate function:

const replaceA = (rows, flag) => (
flag
? rows.map(row => ({ ...row, a: 'test' }))
: rows
);
const processRows = (rows, flag) => {
const newRows = replaceA(rows, flag);
... more processing
return finalRows;
};
const result = processRows(rows, true);

Now, because we have extracted the conditional processing to a separate function, we do not need to re-assign any variable: we are always creating a new one, that we’re going to use for the subsequent processing. Each step of the processing is going to, in the same fashion, return a new variable until we have our finalRows that we can return.

But now, we just created ourselves a new problem: say we have 3 functions we need to apply to our data, our main function is going to look like this:

const processRows = (rows) => {
const stepA = applyStepA(rows);
const stepB = applyStepB(stepA);
const stepC = applyStepC(stepB);
return stepC;
};

Those are a lot of variable assignments, this code is way too verbose and is becoming hard to maintain: each function has to use the data from the previous step, or it will be inadvertently discarding one step of the processing.

Let’s tackle this with a simple pipeline util function:

const pipeline = (...fns) => seed => (
fns.reduce((acc, fn) => fn(acc), seed)
);
const processRows = rows => (
pipeline(
stepA,
stepB,
stepC,
)(rows)
);

We can even go further and write it “point-free style”:

const processRows = pipeline(stepA, stepB, stepC);

Now we got ourselves some clean, terse, functional JS code, but we’re still copying our rows multiple times ; time to bring in the big guns!

Let’s go back to our initial example, and see how it would look like after applying those FP patterns and Mori immutable data structures:

import mori from 'mori';const replaceA = (flag, rows) => (
flag
? mori.map(row => ({ ...row, a: 'test' }), rows)
: rows
);
const processRows = flag => pipeline(
mori.partial(replaceA, flag),
... more functions
);
const result = processRows(true)(rows);

Here is what’s happening: first of all, processRows now takes only one argument, the flag boolean, and returns a function, our pipeline, that itself also takes one argument, the rows of data.
Inside that pipeline, we’re discovering an interesting case where a step takes several arguments. To tackle this, we reversed the order of the arguments replaceA takes so that the data it expects comes last (this is a very common pattern in functional programming). Then we partially apply the function using a handy partial util provided by Mori, so that it expects only one last argument: the data to operate on.

Lastly, we replaced the native map with Mori’s, that also happen to expects data as its last argument (see? 😄).

And boom! We’re not duplicating the input rows anymore! (We’re still duplicating the objects it contains though, and we could avoid that with Mori too, but that’s beyond the scope of this article).

Using functional programming principles and immutable data structures, we were able to break down our code into smaller, more easily testable functions. But most importantly, we are no longer mutating the original array, while avoiding the performance drawbacks of constantly duplicating our input array like the native Javascript array methods like map or filter inevitably do.

--

--