6 min read

The terrible fate of reduce

Banner image

And why Javascript programmers hate it.


Javascript Functional Programming
Published

I recently came across this video on youtube that a friend linked talking about the problems with reduce:

and it gave me some conflicting thoughts which got me thinking. Why is it that the tool that plays such a crucial part in functional programming so hated by experienced developers? Of course, debating menial stuff is a programmer’s favorite pastime but the reduce argument seems to be popping up every single time someone suggests using reduce to solve a problem.

Before continuing, lets briefly go over what reduce actually is

function sumNumbers(numbers: number[]) {
  return numbers.reduce((sum, num) => sum + num, 0)
}

Here, reduce simply takes a list of numbers and goes over them, while changing the accumulator for the next iteration. This is, of course, an oversimplified example but the basic idea is that you have a generic list of items, and there’s an accumulator of some data type that is updated in each iteration. In that case, what’s so bad about it?

Hate the player, not the game

From the video and the discussions I’ve had with people online, it seems like a lot of the hate that’s directed towards reduce is that it’s easy to abuse. There are a lot of ways to use reduce in ways it wasn’t meant to be used, though you could say that it’s possible to blame a lot of different things with that reasoning. It’s not the function itself that’s problematic, it’s just ways that people use it. On one hand that’s true and doesn’t make reduce itself bad, but on the other, programmers read other programmers code and the way people use tools affects the way it’s viewed as well.

But the story of reduce starts way before Javascript, so lets take a look at the origins of reduce itself and why it feels like a monstrosity to developers.

Functional Origins

Reduce is actually somewhat of a more modern name for the same concept found in many functional languages referred to as a fold. Like many other popular features like lambdas and higher order functions, the idea of folding is borrowed from languages like Haskell and Lisp because they are very convenient to use in those languages.

However, I want to argue that the convenience of functions and concepts in paradigms aren’t solely based on the merit of the idea itself, but are closely tied to how they fit within the paradigm they’re used in.

Consider the Array.prototype.flat rewritten in Typescript that’s written using reduce

function flat<T>(arrays: T[][]): T[] {
  return arrays.reduce((all, arr) => all.concat(arr), [])
}

It’s not too bad, I would argue reduce is the right tool for a function like this, and now the same function written with a regular for loop

function flat<T>(arrays: T[][]): T[] {
  const out: T[] = []
  for (const arr of arrays) {
    out.push(...arr)
  }

  return out
}

You could argue that the for loop version is longer-ish or that the reduce doesn’t give you any benefit and both of those would be reasonable thoughts. If reduce is such a “good” concept, why is this a controversial argument? Higher order functions are a good concept and we don’t spend nearly as much time debating those.

The answer to this is in how JS and other popular languages try to fit features that don’t work well in non-functional paradigms in their language.

Reduce sucks but fold is king

flat :: [[a]] -> [a]
flat = foldl (++) []

This is the Haskell version of the same function above and it looks… comical in comparison. Some questions that may arise in reader’s mind when looking at this example are:

  1. How is this so much shorter than the JS version?
  2. What the hell is going on here?

    sana confused

We’re essentially doing the exact same thing as the first one JS example

flat :: [[a]] -> [a]

We’re stating that a flat function takes an input of 2 nested arrays and returns a singly nested array:

flat = foldl (++) []

And that flat is defined as a reduce of the input where the function is (++) (array concatenation) and the initial value is [], an empty array.

The reason why this is so much shorter than the JS counterpart doesn’t actually have anything to do with Haskell specific syntax. It’s possible to write reduce in this exact way in Javascript

const flat = arr => reduce(concat)([])(arr)
// or we can reduce this definition to just
const flat = reduce(concat)([])

flat([
  [1, 2, 3],
  [4, 5],
])
// [1, 2, 3, 4, 5]

But we choose not to because it doesn’t feel natural in JS (Also Typescript, as of today with 3.9 isn’t able to infer types of curried functions very well but that’s a different story). If you’re an epic category theorist this is fine but it can be a problem if your coworker submits a pull request with code above and you can’t help but feel like he’s challenging you to a high IQ competition. It seems like a big part of the debate revolves around people who are used to a functional style trying to shoehorn functional patterns into every language they use vs js programmers who are either not familiar with FP or don’t want to use it in JS.

So what do we do

It seems like the syntax alone does not solve this problem. Fold/Reduce seems to be a good concept that works in the right languages, but not so much in javascript.

I think reduce is fine to use, don’t try to use it like you’re writing Haskell but the idea is valuable. If your code is too smart, that’s a problem that goes beyond just reduce. Reduce is innocent.

I also think Javascript should have put the seed value of reduce as the first argument, not the second. In functional languages with currying, it’s valuable to have the seed value as the second argument because your code is more modular when you leave the data to be inputted last so you can pass it as an argument later, but this has no use with JS. It would’ve been much better to have reduce that goes:

function sum(nums: number[]): number {
  return number.reduce(0, (all, num) => {
    return all + num
  })
}

This way the initial value and the function are all in the same place and you don’t have to go up and down the function call to see what you’re working with.

In essence, just be responsible with your code and it doesn’t matter whether you’re using reduce or the fancy function from next hot new paradigm.