Hacker News new | past | comments | ask | show | jobs | submit

Anecdotally, programmers dislike "reduce"

https://evanhahn.com/posts/2026-09-13-programmers-dislike-reduce/
loading story #49738181
Map and filter usually have only one arg and if they have 2, the 2nd is almost always a 0-based index. They look identical in most languages, even when Microsoft chooses to call them Select and Where.

Reduce has an accumulator and a 2-arg function and languages are not very consistent amongst each other as to whether it's reduce(initial_acc, callback(acc, elem)) or reduce(callback(acc, elem), initial_acc) or reduce(callback(elem, acc), initial_acc) or what.

Hard to remember. Also some languages have a version of reduce that doesn't take an initial accumulator at all, which is just a footgun waiting for you to hit an empty collection. Also ALSO, the accumulator can easily become awkward in languages that don't support anonymous types or don't support easy mutation of an anonymous type record. Which is most of them!

Haskell got this right. You have foldr (right fold) and foldl' (left fold), and the order of the callback is opposite. If you do a left fold, then the initial accumulator is applied on the left; if you do a right fold, then the initial accumulator is applied on the right.

    foldr f z [x1, x2, ..., xn] == x1 `f` (x2 `f` ... (xn `f` z)...)
    foldl' f z [x1, x2, ..., xn] == (...((z `f` x1) `f` x2) `f`...) `f` xn
The mnemonic here is that the folding function (aka the callback) replaces the comma.

I find this slightly easier to remember than other languages. In contrast most other languages do not simultaneously provide a left fold and a right fold, so they do not consider this aspect, making things more difficult to remember.

That said I totally agree this requires more brainpower to read and write than map or filter. For this reason I have sometimes refactored code to use foldMap instead of foldr or foldl', so one no longer needs to think of the direction of the fold or the order of arguments.

loading story #49738128
loading story #49737013
I admit that I have always looked at an explanation like yours with x1,...,xn when using fold because I could never keep it straight in my mind.
loading story #49736324
loading story #49736568
loading story #49733695
Also reduce is a weird name.
loading story #49736554
loading story #49738068
{"deleted":true,"id":49735975,"parent":49707608,"time":1789614683,"type":"comment"}
loading story #49733452
It doesn't help that fold/reduce often have different orders depending on the ecosystem. Every few months when I have a reason to reach for `fold` in nutshell I forget that it has the next element as the first arg instead of the second, which is what I'm used to from Rust. I guess I should just be happy I don't need to specify which direction I want like in OCaml.
> Map and filter usually have only one arg and if they have 2, the 2nd is almost always a 0-based index. They look identical in most languages, even when Microsoft chooses to call them Select and Where.

I don't understand. Map takes input of type a and size n and returns output of type b and size n.

Filter takes input of type a and size n and returns output of type a and size ≤ n.

They look nothing alike?

loading story #49736338
loading story #49734654
loading story #49731007
loading story #49735709
loading story #49708207
Reduce is like `fold` in Haskell right? Fold in Haskell has many variant, I forgot exactly which, but I remember there were many.

I never met so many different variants of the `map` or `filter` function in Haskell.

Maybe this shows, in a different way from the reasons in the article, why reduce is harder than map/filter.

loading story #49707808
loading story #49697267
loading story #49706335
loading story #49733721
loading story #49731314
loading story #49733196
loading story #49693507
loading story #49737577
(JS/TS is my main language) I love reduce()! It's a hammer/nail method for me. Everything looks like a problem solvable by reduce. (I'm often wrong on that, but I quite enjoy learning why by trying).

I really like taking the implementation away from the call site, so that the call site reads

    const myNewValue = data.reduce(doSomethingMagic); 
(and then `doSomethingMagic` is defined somewhere else). So simple.

I failed a job interview once by using reduce() in a coding test. The reviewer didn't understand why I hadn't used a loop. Loops are easier, for sure, but they sprawl and are open to hacking. They can bring in state from outside the loop. They make the call site long (you always have to read the implementation to learn that you don't need to read it). The same interviewer actively liked to have loop bodies modify the loop conditions (e.g. by taking items out of the source array and decrementing the end condition, so the loop would end earlier). That's the kind of "clever" I find unpredictable and hard to think about. Probably a good thing he rejected me.

> The same interviewer actively liked to have loop bodies modify the loop conditions (e.g. by taking items out of the source array and decrementing the end condition, so the loop would end earlier).

Wow. That is the kind of monkey business that would have me running for the exits. Yikes.

loading story #49734446
loading story #49733713
You didn't get that complaint in Clojure, just like you wouldn't in Scala or Haskell, is that once you have any expectation that your users know a little bit of category theory, and possibly also thinking in types, it's all quite easy. Even fold is kind of easy, with the more complex signature. But passing [A][A,A =>A] kind of sucks for those that don't think of functional programming. and [A][A,B => A] is even worse. It's often bad enough to get people to build a comparator.

Every industry language keeps gaining more and more functional features: Many a new Java version is adding a bunch of scala features with worse syntax. But we don't train people on functional programming at all, so by the time they've built their instincts, passing functions makes no sense to them, immutability is alien, and the idea of a pure function seems irrelevant to them. Thus, they don't get exposed to the building blocks that make reduce seem simple. We always teach them recursion, but the rest? Too little, too late.

I could tell you of a bunch of ways to simplify the signature by, say, mandating that one passes a monoid or something like that, but while the signature would be easier, the very same people that are only used to imperative OO will not have an easier time, because they might have studied 2 years of calculus, but they've never even smelled abstract algebra. You can walk out of not just a programming bootcamp, but many a computer science degree without learning a word of this. Therefore, it all remains complicated.

loading story #49736247
loading story #49735348
loading story #49731211
loading story #49736983
I am always happy when I find an opportunity to reduce or zip, so handy.

I also like Lodash'es transform[1]. It's like reduce, but expressly for transforming one collection to another. The signature is a slightly different from reduce in that the accumulator is a collection that is passed as an argument to the iteratee who is expected to mutate the accumulator with no need to return it. This frees up the return value from the iteratee for a new purpose: if the iteratee returns a boolean false, then transform early outs. I have used that feature more than once!

[1] https://lodash.com/docs#transform

loading story #49702635
loading story #49733212
loading story #49733465
loading story #49737491
loading story #49736380
loading story #49702991
loading story #49734286
I've seen a lot of technical points about reduce, all of which are true.

But I think the real reason might be even simpler: you can't tell what it does just from the name. What `map` does is consistent with well-known programming jargon. What `filter` does is consistent with the word's everyday meaning. But if you don't already know what `reduce` does, it's name isn't even enough to hazard an educated guess.

That's not true in Clojure because for lisp programmers for two reasons. First, `reduce` is a ubiquitous and well-known concept in lisp.

Second, in most lisps manually doing the same task with imperative code is an ugly verbose eyesore. But in algol-style languages, the imperative alternative is only 1-2 extra lines of very simple code, so using `reduce` is arguably just code golf.

loading story #49733885
loading story #49735088
loading story #49733459
loading story #49737515
loading story #49733344
loading story #49733588
loading story #49703118
loading story #49735292
loading story #49731160
It's part of the functional trio: map, filter, reduce--and half of MapReduce.
loading story #49734504
loading story #49733590
loading story #49734439
loading story #49736267
loading story #49734225
loading story #49696160
loading story #49734317
My favorite gotcha is Java's `Stream.reduce(accumulator)` doesn't call the accumulator if your stream has zero or one elements. This is used for `min(comparator)` and `max(comparator)`. It's very funny when the comparator throws, but only when you have 2 or more elements.
How is that a gotcha? If there aren't two elements how could you possibly expect a function with two arguments to be called? What would you call it with?
loading story #49734934
I’m so confused, how are you supposed to perform aggregation without reduce? This is like saying “I like plus and times, but I don’t like divide because it’s hard.” I mean sure, but you need it??
loading story #49735215
loading story #49731311
I'm the weird one here. In JS at least, I reach for reduce before map and filter in most cases. Often it is because I want the accumulator, particularly when I have a list of objects with various properties that I wish to sum together in a reduced object.
loading story #49735487
loading story #49733594
loading story #49702010
loading story #49734882
loading story #49734717
loading story #49734817
loading story #49731590
loading story #49731110
loading story #49700854
loading story #49731192
loading story #49737392
loading story #49702709
Sure, I'm up for some bike-shedding. [0][1] Unless performance demands otherwise, I prefer map+filter because:

1. It's cheaper/faster at communicating intent to humans reading your code. Since a reduce call can do all sorts of interesting things, people need to stare harder to realize "oh, it's just doing a a map and filter together."

2. Things are easier to debug. I can vet the process of transformation (and its intermediate results) and then vet the process of excluding some of those results.

_____

With respect to debugging, a sample form Elixir's REPL where the piping (|>) to the dbg() function reveals the intermediate state:

    iex(1)> [5,34,6,2,7,3,1] |> 
                     Enum.map(fn x -> x * x end) |>
                     Enum.filter(fn x -> x < 10 end) |> 
                     dbg()

    [iex:4: (file)]
    [5, 34, 6, 2, 7, 3, 1] #=> [5, 34, 6, 2, 7, 3, 1]
    |> Enum.map(fn x -> x * x end) #=> [25, 1156, 36, 4, 49, 9, 1]
    |> Enum.filter(fn x -> x < 10 end) #=> [4, 9, 1]


[0] https://en.wikipedia.org/wiki/Law_of_triviality

[1] https://www.smbc-comics.com/comic/noun

loading story #49698096
loading story #49696673
Not really, programmers only educated in traditional imperative programming I would assert.
loading story #49737217
I've always like reduce myself, didn't realize others had a negative attitude towards it.
loading story #49736887
loading story #49733819
loading story #49734712
loading story #49733863
loading story #49731250
Alternative theory - reduce is badly named.

combine, accumulate it aggregate would have way more use.

loading story #49731852
loading story #49735266
loading story #49733550
loading story #49731237
loading story #49731036