Using functional patterns in Javascript, Part Two

A week ago, I introduced my current thinking on how to use functional patterns in my daily Javascript usage.

The working example was a micro-service that accepts a customer order and processes it. We had a look on how to use an Either-like construct to validate the incoming message and build up a data structure for processing.

Today, I want to examine how to do the processing using the map/reduce pattern, with a special emphasis on the "reduce" part of the combo.

Avoiding the dreaded for-loops

You've seen this a thousand times.

var items = methodThatProvidesItems();
var output = []
for (var item of items) {
var otherData = methodThatEnrichesItem(item);
var transformed = { name: item.name, price: item.price, description: otherData.comment, ... };
output.push(transformed);
}
// presumably do something with `output`

This works sort-of OK when the for-loop is maybe 3-4 lines long, tops. After that it quickly becomes messy and harder to read.

Plus, the code inside the for-loop manipulates the output variable, which, even if declared with const, is mutating.

A simple variant of this code sample using an immutable variable could be as follows:

const output = methodThatProvidesItems().map(item => {
const otherData = methodThatEnrichesItem(item);
return { name: item.name, price: item.price, description: otherData.comment, ... };
});

Now the output variable is declared and initialized in one step and can be considered immutable, if only by convention you'll have to enforce (no read-only lists in Javascript, alas).

Complicating things with filtering

Let's consider a more complicated logic. Say you want to filter out items that don't have a price.

for (var item of items) {
if (item.price <= 0) continue;
var otherData = methodThatEnrichesItem(item);
var transformed = { name: item.name, price: item.price, description: otherData.comment, ... };
output.push(transformed);
}

Using filter and map methods on the Array, you can do this in two steps:

const output = methodThatProvidesItems()
.filter(item => item.price > 0)
.map(item => {
const otherData = methodThatEnrichesItem(item);
return { name: item.name, price: item.price, description: otherData.comment, ... };
});

That's how I would do it previously. Confession: I have never been a performance freak. 🤣

Things can get ugly when there's more than two map and filter steps, which can easily happen in a more complicated pipeline.

Within each map and filter step, a new list is created, while in the end, you are only interested in the final list, or rather, in the output of the last map or reduce step.

This is where reduce comes in handy.

Reduce right away to slash the number of iterations

When first getting familiar with reduce, I was always looking at the numerical examples, such as summing the values:

var items = methodThatProvidesItems();
var totalPrice = items.reduce((acc, i) => acc + i.price, 0);

The good thing is, the reduce method of Javascript arrays can produce any kind of value, not just a number! Let's rework our example and replace the filter and map calls with a single reduce:

const output = methodThatProvidesItems()
.reduce((items, item) => {
if (item.price <= 0) return items;
const otherData = methodThatEnrichesItem(item);
items.push([{ name: item.name, price: item.price, description: otherData.comment, ... }]);
return items;
});

Here, you only iterate over your list once and are done. Better!

(Arguably, the items variable in this example is a mutating one! Perhaps a cleaner approach would be to return items.concat([{...}])) but I'm not sure if the potential performance penalty of creating a new list with each iteration is worth it).

Summary

The map/reduce pattern is a wonderful tool that obliterates for-loops in many scenarios, reduces the need for mutating variables, and makes it easier for an elegant, clean code to emerge.

The pitfall of using multiple map (or filter) calls in your pipeline is performance hits due to multiple iterations. You can solve it by replacing these steps with a single reduce call.

A more complex example would call for refactoring the lambda inside reduce to a standalone function (or more). I think that's beside the point here, however.