I think it's because reduce is usually too powerful. Map and filter are very focused operations. But reduce is actually pretty powerful. In fact, it's so powerful, that if you take it's function signature, you can actually just make an equivalent list type: type List a = forall b. (a -> b -> b) -> b -> b! (Good exercises for the reader would be to implement things for that list)
I think you're usually better off defining or finding a better operation than using reduce directly if you can.
The power asymmetry is also clear in that you can easily define the other two in terms of reduce (using foldr and Haskell syntax here since I'm lazy) but not vice versa:
myMap f xs = foldr (\x acc -> (f x):acc) [] xs
myFilter f xs = foldr (\x acc -> if (f x) then x:acc else acc) [] xs
In general it's definitely possible to use reduce in a somewhat confusing way whereas map and filter are always straightforward.
Yes. Graham Hutton has a wonderful paper from 1999 called A tutorial on the universality and
expressiveness of fold showing how fold/reduce is the universal abstraction for structural recursion over lists (or any inductive data structure, really).
On top of being too powerful, I think the ergonomic tax on working memory is too high. Every language has its own operand order and function syntax, so now when writing your reduce, you have to consider:
Is this a right fold or a left fold? Does it matter?
Is the first argument the base accumulator or the reducer function?
In the reducer function, which argument comes first, the element or the accumulator?
That's already a couple slots of working memory that need eviction in order to write the fold. What gets evicted is probably the domain logic that we're trying to express in the fold.
It doesn't help that every language has a slightly different take on the above questions.
Yup, this. In the time it takes me to look up the argument order and figure out if it matches the functions I'm composing, I've already written the explicit loop.
I also find that an explicit recursive loop is much easier to write (in either Scheme or Clojure) and allows one to have more than one accumulator. And if you need an extra accumulator or somehow different behaviour in some edge case, you'll end up tearing it apart and rewriting the body as an explicit loop anyway.
It's basically the same reason I never use do in Scheme - where goes what is always confusing.
And the funny thing is, it doesn't occur that often that you have it readily available in memory. When I was just beginning to learn Scheme, I made a point of using fold more often, and I found it easier back then because I'd been using it more often. Now, after a few decades of working with Scheme I don't even bother anymore.
You can have more than one accumulator by making your fold accumulator a tuple of accumulators, but yeah that makes it just hideously complicated because you also need to express when the fold doesn’t change a given accumulator in the tuple.
I just came off a stint writing a bunch of OCaml and making a complicated reduce could single-handedly fry me for the rest of the day.
I think this explains the "reduce is harder to read" factor.
Code with too much expressive power is harder to read. When you are reading and come across map or filter, it narrows the space of possible things the code could do. reduce doesn't do that at all, because it is the most general list function (that's exactly what that Böhm-Berarducci definition of List shows). It could do anything!
Of course, by that argument, you shouldn't replace reduce with a written-out loop (since a loop is even more expressively powerful), so familiarity is also likely a factor.
You should replace it with other common reduces like sum, all, etc., or give your reduce a name.
I think yes and no. The power might be related, but the simple for loop is even more powerful. However, for loops that mimic a simple reduce (e.g. sum, product, etc) can be quite readable.
The worst for loops are quite unreadable, of course. My point is just that this isn’t just about power, it’s also about syntax/pattern recognition.
I can't come up with a way to write down your type signature in (Isabelle's) simple type theory, if you write something like type_synonym ('a, 'b) List = ... equalities such as len (cons x xs) = 1 + len xs don't hold.
(Aside: I think this is because, intuitively, you have to specify the type of 'b up front, and then len can choose to pick a random element from nat instead of returning one of the elements it has at its direct disposal. Here, len's signature is ('a, nat) list => nat, so it's free to return 1, 2, 3, and so on, based on which list you give it. You really need parametric polymorphism to make this work, i.e., have some kind of guarantee that functions operating over 'b will not make up random elements of 'b.)
So it seems that the power of reduce sits right beyond the edge between simple type theory and beyond. Which also mirrors the increase of brainpower needed to work with more powerful type systems such as Haskell's or Lean's (at least, if anecdotal complaining about Haskell and Lean being difficult is any indication).
For those who want to read more about this: this is called the Church encoding of lists. ehamberg's link to Hutton's paper is also a good read in this direction.
There's definitely something to that, but I think it's also partly the syntax. Pyret[0] has nice syntax for fold, and I find it much easier to use in Pyret than in other languages as a result. You can write:
my-list = [list: 1, 2, 3, 4]
sum = for fold(total from 0, n from my-list):
total + n
end
print(sum) # 10
I find this easier to read than the thing it's sugar for:
my-list = [list: 1, 2, 3, 4]
sum = fold(lam(total, n): total + n end, 0, my-list)
print(sum) # 10
(Pyret's for isn't specific to fold, it's general and works with map and filter too.)
I think it's because reduce is usually too powerful.
How much I dislike reduce is closely related to how much of that power is being used.
If it's a simple associative operator with an obvious identity, it's totally fine.
If it's something with a clear algebraic structure that is best understood as an algebra, well, at least it's the appropriate tool. If I already need to read a bunch of category theory about actions and modules to understand a non-associative operation, then sure, use the reduce.
If the code is using the reduce to make something tail recursive, and passing simple accumulator values, sure, OK.
Once the reduce is starting to accumulate a list, then I'm starting to wonder if this wouldn't have been clearer as a named function that case matches over some input.
However, if the "reduce" mutates impure state outside the reduction, then yeah, I'm probably going to try to use it as a teachable moment. I used to see this all the time in Ruby.
Similarly, any Python comprehension that's mutating external state is a no go. Functional tools should not be mixed with mutation.
So I guess my personal bar for using reduce is "this operation is most clearly understood as a purely functional reduction." And very often, there's a more straightforward way of looking at most operations, at least outside deep Haskell.
In APL-family languages, "reduce" is the first "loop-like" construct you encounter. K uses / ("over") for reduction and \ for its counterpart, "scan", which produces all the intermediate results:
+/3 4 5 6
18
+\3 4 5 6
3 7 12 18
Being able to substitute \ for any / and view a trace of computation is very handy for teaching and learning K. A surprising number of languages which offer "reduce" do not also provide a "scan".
Agreed that the symmetry of / and \ is a powerful learning and development tool in array languages.
I believe APL's reduction / is easier to understand than the examples of reduce from languages mentioned elsewhere in this thread, because it is expressible in a straightforward syntactic way:
-/3 4 5 6
...is the same as "put - in between each element of the array":
3 - 4 - 5 - 6
In APL, this is equivalent to:
3 - (4 - (5 - 6))
...which evaluates to negative 2:
-/3 4 5 6
¯2
In K, / processes in the other direction:
-/3 4 5 6
-12
(This behavior becomes more complex and varied across array languages once a left-hand argument is supplied.)
This has been a great joy and help in learning K. It aligns with the quick iterations thesis that underpins the process of writing K and it makes many of the common pitfalls that reduce can introduce easily avoidable.
I think the other explanations given in this thread are bigger factors, but there's also the fact that reduce is a terrible name. I think fold is marginally better but not by much.
In the Fennel programming language, due to design constraints we can't add functions to the core language, but can add macros. (It's complicated and not really the point here.) When the time came to add a reduce-like operation, we looked at alternative names. I had heard somewhere that Smalltalk used the term inject for this (which I think is even worse than reduce) but also offered the alternative of accumulate, which I think is much better, and we eventually went with that.
However, now that I'm looking for a source for this, it doesn't seem accurate; as far as I can tell Smalltalk only has inject. I'm not sure where I heard this! What other language calls it accumulate?
C++ has std::accumulate. It also has std::reduce and std::ranges::fold_left though and those are all slightly different so....yeah.
Ruby also uses inject which is what I first learned, though reduce makes the most sense to me. In the end I just think of it as "map + state, but return the state". There's good reason to not think of it that way, but it was easier to think about until I grokked it more holistically. As yet another alternative name, Ruby also has Enumerable::each_with_object which is different mechanically but practically gets at the same idea.
The thing about accumulate is there are other uses of the term that are similar but different. IIRC Python uses accumulate to mean scan, so it returns each intermediate value as it reduces. Julia does the same thing. I actually thought this is what the Smalltalk version you were thinking of was going to turn out to be...but I can't find any reference to it.
The thing that I really appreciate about Ruby inject is how nice it makes the simple case where you’re returning the same type as the values. It’s almost a different function because you don’t have to worry about the accumulator at all.
For example
[1, 2, 3].inject { |a, b| a + b }
inject “injects” or inserts the “+” in between every item in the array. It’s easy to read as 1 + 2 + 3. Obviously this doesn’t cover every case of reduce. But when I can think of an operation as “injecting a binary operator between pairs in the list” it’s nice because I don’t have to worry about the accumulator.
Even this is doing too much, because Ruby's inject supports:
[1, 2, 3].inject(:+)
But now that's just a wordier way of writing sum. The reason I would use inject / reduce is specifically that I want to accumulate the operands by iteratively returning the sum, and the logic required to combine them is too complex for an existing Enumerable method. This may be part of the reason why people dislike reduce: it's really only necessary in edge-case situations that are hard to understand.
IIRC #inject is one of the few methods to support a bare atom like that where it doesn't call #to_proc. But you're 100% correct on #sum, shouldn't use #inject/reduce for that.
I tended to stay away from reduce/inject in Ruby as I found it confusing, but then when #each_with_object came along I ended up using it a lot. I think its automatic return of the memo'd collection is very ergonomic.
Fennel is a lisp that compiles to Lua. It ships both a runtime loader that compiles code as it's read, but also as an ahead-of-time batch-style compiler. I assume that this is for cases where your Lua-using program is fussy about how it takes your extensions (e.g., mods for proprietary games) and so you can't count on loading some hypothetical Fennel runtime library before loading any of your code.
We could of course add a "standard library" containing functions, but other people have already built non-standard "standard library" libs for Lua already, and theirs are quite good, so there wouldn't be much point in reinventing the wheel when it would sacrifice the zero-runtime property we currently have.
"aggregate" is both shorter and less addition-tinged, but (against it), "aggregate" can be both a noun or verb.
In statistics things like min, max, sum, average, and higher moments are called "summary statistics". So, "summarize" is a not oft mentioned possibility, though that creates a slight ambiguity over incremental/one-pass summarization as is often meant by "reduce" and bulk/multi-pass summarization { e.g. first calculate mean, then mean (x_i - mean)^2 }. This incremental-vs.bulk distinction is another aspect/view of the complexity others are observing here.
By that point I think Calvin wasn't hanging out in our channel. But it's possible that a Janet user was in the chat and suggested it. On the other hand, even tho Smalltalk doesn't have it, I had a memory that it did based on a random IRC conversation from over a decade beforehand, and that might be more likely to be where it came from.
reduce is less elegant in languages I use, like JavaScript, Python, and Swift.
Anecdote from Python-land: reduce used to be a built-in function in Python 2, but in Python 3 it was relegated to functools. Why Guido decided to demote it:
This is actually the one I've always hated most, because, apart from a few examples involving + or *, almost every time I see a reduce() call with a non-trivial function argument, I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do. So in my mind, the applicability of reduce() is pretty much limited to associative operators, and in all other cases it's better to write out the accumulation loop explicitly.
So Evan is not alone in noticing this!
Contrast with map/filter, whose Pythonic equivalent is comprehensions. Anecdotally, those seem to spark joy more often. (I like them, at least.)
I've noticed the usefulness of reduce is massively reduced when working with imperative data structures. The python explanation says it's most useful involving + or * which makes sense because in python numbers are stable values and data structures are not.
result = None
for elt in some_data:
result = do_things(elt, result)
The extracted body is rarely that useful in itself. Meanwhile the alternative is easy to tweak. Things like skipping over elements become a continue call (reducer can of course no-op by returning the accumulator of course). You're just looking at the operation (no indirection due to method naming)
You're right in that in most imperative systems it's trickier because ownership is unclear ("should I copy"?), among other things. I do think tho that in Rust in particular you can get away with it because of ownership tracking (and be quite performant as well?)
I would posit that accumulation as a pattern is simply solved decently in languages like Python in a lot of "canonical" cases and thus the common reduce patterns... become sum if you like method overloading.
I am very fond of reduce but I agree with the anecdotal experience. The problem with very general tools is that they require you to see the world in terms of very general abstractions, and that's rarely actually necessary for most line-of-business stuff and so is a rarely exercised skill.
I've been on a long term quest to eliminate the major use cases from JavaScript - so far Object.fromEntries (wasn't ever actually a good use case for reduce but people kept doing it anyway), Math.sumPrecise (was originally going to be just Math.sum but we ended up with the high-precision version after committee, oh well), and Iterator.prototype.join (not quite merged but probably this month); next up is probably Math.argmax although there's a lot of other things I want to get through first.
Caveat: My experience is mostly Haskell and while I'm sympathetic to lisps, I've done very little with them. I find the contract for clojure.core.reduce rather difficult to follow:
If you call reduce with two args, it behaves kinda like foldl1 f coll:
It computes (f (f x1 x2) x3) etc., with the first elements of the collection being in the first applications to f.
If you call it on an empty collection, you get (f). (My read is that the library author tried to make reduce total when used in two-arg form, at the cost of needing f to work with zero and two arguments, but IMHO it's really a precondition violation. I feel raising an error would be more appropriate.)
If you call it on a singleton collection, you get that value back and f is never called.
If you call reduce with three args, it behaves kinda like foldl f val coll:
It computes (((f val x1) x2) x3) etc. As with the two-arg form, earlier elements are applied first.
If you call reduce on an empty collection, it returns val.
The thing that seems most surprising a that (reduce f z [1]) is (f z 1) but (reduce f [1]) is 1; and that (reduce f []) is (f) but (reduce f z []) is z. The reader is forced to care about boundary conditions much more than when using map and filter. It's clear why after you think about what each form of reduce is doing, but I personally would have distinguished between possibly-empty and definitely-non-empty collections and provided reduce and reduce1 functions in the stdlib.
Who knows what the semantics of reduce are when you call it with a collection and no initial value?
[Audience response]
No one, right. No one knows. It's a ridiculous, complex rule. It's one of the worst things I ever copied from Common Lisp was definitely the semantics of reduce. It's very complex. If there's nothing, it does one thing. If there's one thing, it does a different thing. If there's more than one thing, it does another thing. It's much more straightforward to have it be monoidal and just use f to create the initial value. That's what transduce does, so transduce says, "If you don't supply me any information, f with no arguments better give me an initial value."
Note though, there's no requirement that you support that. You don't have to have that arity. You don't have to allow people to call transduce with no initial value because sometimes there's just no good made up from nothing initial value. Somebody needs to think about an initial value or supply some inputs to your process to get a starting value. Not everything can be made from nothing. It's easy to come up with zero from nothing. But it's not easy to come up with a channel from nothing or other kinds of things, you know, event systems. You don't have to support this.
That's what transduce does, so transduce says, "If you don't supply me any information, f with no arguments better give me an initial value."
So the function is expected to both serve as the combining (<>) operator (aka mappend) when called with two arguments, an as the identity (aka mempty) for the combining function with no arguments? That sounds like it's being asked to serve as the dictionary for a Monoid instance but instead of passing in an explicit interface (or having the compiler do it for you), the function is overloaded to handle both zero and two arguments, but it's okay to not provide the initial value in certain instances? I still feel some heebie-jeebies at that design decision.
the function is overloaded to handle both zero and two arguments
That's right. I imagine it's downstream of the old Lisp convention where (+) yields 0, (*) yields 1, and so on. + and * support any number of arguments, with zero and two as special cases.
Is this a syntax issue? Curiously, the article doesn't give any code examples at all.
In my own language, Varyx, I made these infix operators with terse names: map, ver, and per. Here are some real-world examples:
let hms = [tm.hour, tm.min, tm.sec] map left-pad % "0" % 2 per {a ":" b}
let config_lines = data.lines() ver bool per comma-separated or "()"
let ensemble = [vox1, vox2, vox3, vox4] per multiplex
The infix orientation was a deliberate choice to allow you to chain operations without nested function calls, with data flowing from left to right (in contrast to Perl's map and grep, which have the data on the right). Complex chains can be stacked vertically:
let x = scores ver { v.value == 2 }
map .key
per Math.product
(Making each of these operator names exactly three letters was also intentional.)
I gather from your second example that per returns some kind of nil value in case the collection is empty, and you can use or to supply a default value in this case. Is that accurate?
What if you want to naturally return nil from the reduction for some reason, but you also want to handle empty collections somehow?
I gather from your second example that per returns some kind of nil value in case the collection is empty,
If the incoming sequence is empty, per evaluates to the empty list, (). The or operator has lower precedence and works exactly like you'd expect: If the entire A ver B per C expression is empty (due to an absence of non-blank lines), you get "()". (The resulting string is subsequently parsed as an expression.)
and you can use or to supply a default value in this case. Is that accurate?
Yup! Nailed it.
What if you want to naturally return nil from the reduction for some reason, but you also want to handle empty collections somehow?
In that case, I might use ternary logic. The operators got delayed while I mulled syntax, but once they're implemented: sequence ? sequence per {...} : alternative. Or if the sequence is a complex expression that you don't want to repeat, if let s = sequence then {s per {...}} else {alternative}.
IMO, just a skill issue: any experienced programmer should have a sold grasp of folds, why are general, how to read and write code using them, since they are generally better than explicit recursion. But a lot of people don't learn the concepts essential to the trade.
I can only agree. I'm used to seeing and writing folds quite often, and I more often than not find them easier to comprehend and grasp the intent of than bare loops. It really depends on the paradigm one was socialized in.
Though I'm sure that an outlier will respond to this comment, every coder that I've met who claims that folds are a code smell will also claim that explicit recursion is a code smell.
i think reduce is not as intuitive because the output which the structure "reduces to" can be any type in general. that's not the case with maps and filters, which always produce the same structure as output.
this makes reduce or fold a bit too general when we see it for first time.
also, with laziness, as in Haskell (foldr v foldl), it becomes extra confusing to reason operationally about.
I can no longer remember the names, but I distinctly recall one rust contributor observing that sum exists because another early contributor really hates reduce.
I think part of the reason is that reduce is less ergonomic in languages without currying, partial application, and a lot of nice combinators lying around in general.
I teach people Clojure, and reduce is the first thing novices latch on when they start learning, mainly because it allows them to reproduce for-loop semantics that they are familiar with from imperative languages. I make them avoid reduce no matter what, in order to push them to use higher-level tools from the language and only use it when there is no other choice.
This. Nowadays List.foldl (\item acc -> ...) initAcc list (or Dict.foldl or what have you) are a second nature, but it definitely took some time to get used to it.
A funny thing I like (in the Elm ecosystem) is that the order of arguments inside the reducing function is the same as The Elm Architecture's update function: the message coming in, then the model you're updating - update : Msg -> Model -> Model ~ foldFn : item -> acc -> acc.
Rather confusingly, the accumulator argument comes first in Haskell (and I prefer this, because it mirrors the inputs) . One can get used to a specific implementation in a specific language, but for a more polyglot/all over the place enthusiasts like me, details like this are one of the reasons I dislike foldl/foldl'/foldr/foldl1 (especially in Haskell, where their interaction with laziness is so unpredictable).
Fold feels like a very powerful but a complex and leaky abstraction. Its natural use is limited to associative operations in pure code, and even there it still can be awkward in practice.
Often, when I’ve submitted a patch with reduce inside, I get a comment like, “this part is hard to read.”
This is terrible PR feedback and you should never leave it at that. This is a purely subjective complaint, maybe you're not good at reading?
The point of giving this feedback is not to dumb down perfectly reasonable code, but to tease out ambiguity. You need to formulate the concern in an objective way that specifies why it's hard to read. If you're unable to do that, then it truly might be a skill issue.
I think reduce is often exposed as an accumulate-like operation that takes one element at a time, which makes it hard. Most C/C++/similar-language programmers use a map-reduce model all of the time: compile is map (source-code file to object-code file), link is map (object-code files to program / shared library). But the reduce phase can't be implemented as an accumulate-like operation, it requires access to each of the objects simultaneously.
I complain about xmake a lot, but I use it because it's less annoyingly stupid than the alternatives. It expresses each high-level build task as a map step and a reduce step, though it calls them compile and link. For a language like Rust, the compile step might be a no-op and the reduce step do the compile-and-link together. For a language like TypeScript, compile might turn TypeScript to JavaScript and then link do nothing. It's a useful abstraction, but the link step isn't written as a lambda that is called once per output from the compile step, it's written as a lambda that takes the complete set of those files as an argument.
This is a first attempt to give words to a notion that has been percolating in my mind for some time.
My programming pedigree is something like Fortran -> Smalltalk -> C -> Python -> Elixir with various polyglot side trips in other hybrid languages. Smalltalk had reduce (called inject: to complete the pentfecta of ..ect methods) and I reached for it less than map (collect) and filter (select). But I was more comfortable with it there than I have been in either Python or Elixir. Part of the reason I think, was that smalltalk has my favorite of the anonymous closure syntaxes. It was lightweight, consistent, and distinct. Other languages have multiple forms for closures and their arguments and are often not as distinguishable from other code flow bodies. So I think that can play a part.
In programming languages, we generally have two basic ways of encoding meaning around variables: by name/binding/keyword or by position. Array vs Dictionary/Map. Ordered function parameters vs named parameters. It has been my experience that position can be the more efficient/terse of the two, but it is the weaker of the two for maintain comprehension.
Reduce usually, but not always, encodes position 1 of the reducer function as the accumulator, and position 2 as the enumerated value. Elixir itself flips them though. If reduce is a “power tool” you get out when needed, one of the first things you have to do is remember which position is which. And since many reduce functions often accumulate the same type as they enumerate, compile time type checking won’t even help you. I find when I’m staring at any non trivial reduce function, that I essentially start thinking similar to when I’m reading assembler, “I’ve got this recursive descent like path, and what’s in what register again?”
In the case of Elixir, I can just make a recursive multi function which will give names much better than a genericsized function formatted in multiple levels of indented block structure. And as soon as the parameter position meanings get two complicated, I just switch to passing named structures. So in elixir, I’ve found that a bespoke reduction communicates better, often formats with really or even less lines of text, and surprisingly is even faster usually.
Reduce is basically an FP equivalent of using a generic loop. Typically, it's better to use a specific higher order function like map or filter which gives an indication of the intent. If I'm reading code and I see filter that gives me immediate context for what the code is meant to be doing. If I see reduce, I now have to walk through the body to infer the intent.
I've been catching up on Sean Parent's talks recently, and one of his big goals for programmers is "No Raw Loops". It comes from C++ world, but I think it's applicable further.
In that light, reduce antipathy is understandable: you have to hold state in your head to ensure that some invariants aren't violated across iterations—in simple maps/filters you have to care about one element only, which shrinks state space considerably.
So the goal (if we're stretching the No Raw Loops further) is to find the actual algorithm and use it—or write one and extract it (and then the implementation may use reduce, OK).
Ironically, I gave this exact feedback to a more senior engineer during a code review many years ago. They wrote something using reduce and I said it was too hard to understand. They were very nice about it, but I wonder if it shouldn't have just been a learning opportunity for me.
In any case, I think it comes down to the function signature. map and filter both act on an array and return an array. reduce acts on an array and returns... any container? It's more powerful, but takes a lot more reading.
I've been more comfortable using it in Typescript recently because the signature is generic so I can be explicit about the output type (and the compiler enforces it).
But I don't like that when returning the accumulator, the simplest thing to do (in JS) is:
acc.someValue = 'whatever'
return acc
Which modifies the original object. In practice this doesn't matter because I'm starting with a blank object. But it's still has the potential for unintended mutation, so the safest thing to do is:
return {
...acc
someValue: 'whatever'
}
But that makes a lot of copies, so it's probably less performant. So then I end up with this whole philosophical debate about how to write a simple function and then I wish I hadn't used reduce in the first place.
I believe part of the friction has to do with empty collections.
Running map or filter on an empty collection is fine and the resulting behavior is obvious: you just get another empty collection back.
But with reduce/fold, you actually need to think about it.
In Dart, there are two separate methods: fold() takes an initial value and successively combines it with the values in the sequence using the given combining callback. If the sequence is empty, you just get the initial value back. But in order to call this, you need to have some sort of meaningful initial value that makes sense.
Then there is reduce(). It only takes a combining callback. It uses the first value in the sequence as the initial value and the combines it with the remaining elements. If the sequence is empty, it's a runtime exception.
Both are useful in different contexts, but you have to do some real thinking to know which one is the one you want.
map() and filter() (where() in Dart), just don't have that edge case to worry about.
I concur, adding my anecdotes to the author's. One note is that it tends to depend on whether the reduce represents a readable high-level concept or is just a weird reformulation of a generic for-loop.
So if I see (in Python):
reduce(set.union, tags)
this is easily readable as "unionize all sets of tags". But if I see (in JavaScript):
something.reduce(
(acc, item) => {
// custom multi-line thing with conditionals, local var mutations and early returns
},
[]
}
then I don't see the point of being "more functional". (Even though I'm totally guilty of doing the latter in the past.)
Anecdotally, can confirm. My colleague and teammate, a very capable staff developer, likes map and filter (in TypeScript) but says he tends to avoid reduce.
Quoting from a note I wrote when we still had juniors in this industry and I wanted them to have a quick reference when to use which array method in JS (especially using reduce in place of flatMap).
I said before, though it is worthy of a repeat, that in my view there are two main ways one can use .reduce. The reducer can take two values of the same type—in which case it exploits the monoidal nature of arrays, as above—or two values of different types, in which case it might be better to rewrite it first with a .map.
I wrote thesetwo tutorials mostly for my colleagues who were looking for a gentle introduction to FP patterns in JS, so they're a bit specific to that language, and I think they might read as too trivial to even be writing about, but they were useful to some people and I think demystified the way one can think about data structures in a functional way.
I once had a manager who loved to use reduce, and every single code review I would ask him to change it, until I finally broke him of the habit.
My primary complaint is that it (typically) saves like 3 lines of code but also makes the code much less readable and harder to reason about. That's a bad tradeoff!
(I finally convinced the aforementioned boss when I reviewed a PR and found a bug that was disguised by reduce but that would have been obvious if he'd just used a for loop.)
I think familiarity definitely plays a role. A typical developer is used to think in side effects, not with types. So when someone sees a function like reduce, they typically try to emulate it step by step, which feels like a more convoluted way of writing a for loop.
Also IMO higher order functions should have the function as the last parameter. Which might be the reason why it's worse in JavaScript and Python. Compare JavaScript:
array.fold(init) {
acc, val => someVeryLongExpression(acc, val)
}
Function as a last argument lets you separate the iteration into a new line, making it look like a looping primitive familiar to the developers. I find it harder to see the array and the default value in the other versions. Whereas in the Scala one I can easily see them and then read the function.
Having the function at the start provides useful support for partial application, which is why Haskell and OCaml do it that way. Python likely picked it up from there, it’s a lot less useful (functools.partial exists but function composition is uncommon), and I think would be as useless the other way around due to the crippled lambdas and reduce being a free function.
Anecdotally, I like reduce()/fold() just fine, but in Rust it's often easier to refactor/reread fold()'s into for loops when you have to change things.
nick4 | 17 hours ago
I think it's because reduce is usually too powerful. Map and filter are very focused operations. But reduce is actually pretty powerful. In fact, it's so powerful, that if you take it's function signature, you can actually just make an equivalent list type:
type List a = forall b. (a -> b -> b) -> b -> b! (Good exercises for the reader would be to implement things for that list)I think you're usually better off defining or finding a better operation than using reduce directly if you can.
quantumish | 16 hours ago
The power asymmetry is also clear in that you can easily define the other two in terms of
reduce(usingfoldrand Haskell syntax here since I'm lazy) but not vice versa:In general it's definitely possible to use
reducein a somewhat confusing way whereasmapandfilterare always straightforward.ehamberg | 10 hours ago
Yes. Graham Hutton has a wonderful paper from 1999 called A tutorial on the universality and expressiveness of fold showing how fold/reduce is the universal abstraction for structural recursion over lists (or any inductive data structure, really).
pzel | 9 hours ago
That paper is my favorite FP paper. Here's my take at implementing the SML Basis List functions via fold: https://pzel.name/2023/07/29/Practical-ML-with-sml-sharp-review-chapter-3.html
FeepingCreature | 5 hours ago
mapis Cfor, butreduceis Cforwith mutable variables.pzel | 8 hours ago
On top of being too powerful, I think the ergonomic tax on working memory is too high. Every language has its own operand order and function syntax, so now when writing your reduce, you have to consider:
That's already a couple slots of working memory that need eviction in order to write the fold. What gets evicted is probably the domain logic that we're trying to express in the fold.
It doesn't help that every language has a slightly different take on the above questions.
sjamaan | 7 hours ago
Yup, this. In the time it takes me to look up the argument order and figure out if it matches the functions I'm composing, I've already written the explicit loop.
I also find that an explicit recursive loop is much easier to write (in either Scheme or Clojure) and allows one to have more than one accumulator. And if you need an extra accumulator or somehow different behaviour in some edge case, you'll end up tearing it apart and rewriting the body as an explicit loop anyway.
It's basically the same reason I never use
doin Scheme - where goes what is always confusing.And the funny thing is, it doesn't occur that often that you have it readily available in memory. When I was just beginning to learn Scheme, I made a point of using
foldmore often, and I found it easier back then because I'd been using it more often. Now, after a few decades of working with Scheme I don't even bother anymore.ahelwer | 2 hours ago
You can have more than one accumulator by making your fold accumulator a tuple of accumulators, but yeah that makes it just hideously complicated because you also need to express when the fold doesn’t change a given accumulator in the tuple.
I just came off a stint writing a bunch of OCaml and making a complicated reduce could single-handedly fry me for the rest of the day.
darichey | 16 hours ago
I think this explains the "reduce is harder to read" factor.
Code with too much expressive power is harder to read. When you are reading and come across map or filter, it narrows the space of possible things the code could do. reduce doesn't do that at all, because it is the most general list function (that's exactly what that Böhm-Berarducci definition of List shows). It could do anything!
Of course, by that argument, you shouldn't replace reduce with a written-out loop (since a loop is even more expressively powerful), so familiarity is also likely a factor.
You should replace it with other common reduces like sum, all, etc., or give your reduce a name.
hyperpape | 15 hours ago
I think yes and no. The power might be related, but the simple for loop is even more powerful. However, for loops that mimic a simple reduce (e.g. sum, product, etc) can be quite readable.
The worst for loops are quite unreadable, of course. My point is just that this isn’t just about power, it’s also about syntax/pattern recognition.
pkolloch | 11 hours ago
Loops are a well-learned concept for all programmers.
Generally, highly expressive, abstract concepts work well if they are used a lot.
Reduce, even if idiomatic for your language, is rarer. And rarer than map/filter as well, while being more complicated.
rbr | 7 hours ago
I can't come up with a way to write down your type signature in (Isabelle's) simple type theory, if you write something like
type_synonym ('a, 'b) List = ...equalities such aslen (cons x xs) = 1 + len xsdon't hold.(Aside: I think this is because, intuitively, you have to specify the type of
'bup front, and then len can choose to pick a random element fromnatinstead of returning one of the elements it has at its direct disposal. Here, len's signature is('a, nat) list => nat, so it's free to return 1, 2, 3, and so on, based on which list you give it. You really need parametric polymorphism to make this work, i.e., have some kind of guarantee that functions operating over'bwill not make up random elements of'b.)So it seems that the power of reduce sits right beyond the edge between simple type theory and beyond. Which also mirrors the increase of brainpower needed to work with more powerful type systems such as Haskell's or Lean's (at least, if anecdotal complaining about Haskell and Lean being difficult is any indication).
For those who want to read more about this: this is called the Church encoding of lists. ehamberg's link to Hutton's paper is also a good read in this direction.
justinpombrio | 4 hours ago
There's definitely something to that, but I think it's also partly the syntax. Pyret[0] has nice syntax for fold, and I find it much easier to use in Pyret than in other languages as a result. You can write:
I find this easier to read than the thing it's sugar for:
(Pyret's
forisn't specific tofold, it's general and works withmapandfiltertoo.)[0] https://pyret.org/
emk | 7 hours ago
How much I dislike
reduceis closely related to how much of that power is being used.reduce.Similarly, any Python comprehension that's mutating external state is a no go. Functional tools should not be mixed with mutation.
So I guess my personal bar for using
reduceis "this operation is most clearly understood as a purely functional reduction." And very often, there's a more straightforward way of looking at most operations, at least outside deep Haskell.Internet_Janitor | 15 hours ago
In APL-family languages, "reduce" is the first "loop-like" construct you encounter. K uses
/("over") for reduction and\for its counterpart, "scan", which produces all the intermediate results:Being able to substitute
\for any/and view a trace of computation is very handy for teaching and learning K. A surprising number of languages which offer "reduce" do not also provide a "scan".semperos | 37 minutes ago
Agreed that the symmetry of
/and\is a powerful learning and development tool in array languages.I believe APL's reduction
/is easier to understand than the examples ofreducefrom languages mentioned elsewhere in this thread, because it is expressible in a straightforward syntactic way:...is the same as "put
-in between each element of the array":In APL, this is equivalent to:
...which evaluates to negative 2:
In K,
/processes in the other direction:(This behavior becomes more complex and varied across array languages once a left-hand argument is supplied.)
regulator | 5 hours ago
This has been a great joy and help in learning K. It aligns with the quick iterations thesis that underpins the process of writing K and it makes many of the common pitfalls that reduce can introduce easily avoidable.
alper | 8 hours ago
I was just asking for this in Roc.
technomancy | 15 hours ago
I think the other explanations given in this thread are bigger factors, but there's also the fact that
reduceis a terrible name. I thinkfoldis marginally better but not by much.In the Fennel programming language, due to design constraints we can't add functions to the core language, but can add macros. (It's complicated and not really the point here.) When the time came to add a
reduce-like operation, we looked at alternative names. I had heard somewhere that Smalltalk used the terminjectfor this (which I think is even worse thanreduce) but also offered the alternative ofaccumulate, which I think is much better, and we eventually went with that.However, now that I'm looking for a source for this, it doesn't seem accurate; as far as I can tell Smalltalk only has
inject. I'm not sure where I heard this! What other language calls itaccumulate?badtuple | 13 hours ago
C++ has std::accumulate. It also has std::reduce and std::ranges::fold_left though and those are all slightly different so....yeah.
Ruby also uses
injectwhich is what I first learned, thoughreducemakes the most sense to me. In the end I just think of it as "map + state, but return the state". There's good reason to not think of it that way, but it was easier to think about until I grokked it more holistically. As yet another alternative name, Ruby also has Enumerable::each_with_object which is different mechanically but practically gets at the same idea.C# (or, well, Linq) seems to use Aggregate.
The thing about
accumulateis there are other uses of the term that are similar but different. IIRC Python usesaccumulateto meanscan, so it returns each intermediate value as it reduces. Julia does the same thing. I actually thought this is what the Smalltalk version you were thinking of was going to turn out to be...but I can't find any reference to it.matthiasportzel | 6 hours ago
The thing that I really appreciate about Ruby
injectis how nice it makes the simple case where you’re returning the same type as the values. It’s almost a different function because you don’t have to worry about the accumulator at all.For example
inject“injects” or inserts the “+” in between every item in the array. It’s easy to read as 1 + 2 + 3. Obviously this doesn’t cover every case of reduce. But when I can think of an operation as “injecting a binary operator between pairs in the list” it’s nice because I don’t have to worry about the accumulator.colonelpanic | 2 hours ago
Even this is doing too much, because Ruby's
injectsupports:But now that's just a wordier way of writing
sum. The reason I would useinject/reduceis specifically that I want to accumulate the operands by iteratively returning the sum, and the logic required to combine them is too complex for an existing Enumerable method. This may be part of the reason why people dislikereduce: it's really only necessary in edge-case situations that are hard to understand.jc00ke | an hour ago
IIRC
#injectis one of the few methods to support a bareatomlike that where it doesn't call#to_proc. But you're 100% correct on#sum, shouldn't use#inject/reducefor that.colonelpanic | an hour ago
Yes, this is because this API long predates the existence of
Symbol#to_proc, so there wasn't any other way to express that in a terse way.technomancy | 2 hours ago
Oh wow, yeah for this one specific case, the name makes a lot of sense. But it's definitely not something you can generalize. Thanks for explaining.
jc00ke | an hour ago
I tended to stay away from
reduce/injectin Ruby as I found it confusing, but then when#each_with_objectcame along I ended up using it a lot. I think its automatic return of the memo'd collection is very ergonomic.NoahTheDuke | 15 hours ago
wtf, care to say more?
jackdk | 14 hours ago
Fennel is a lisp that compiles to Lua. It ships both a runtime loader that compiles code as it's read, but also as an ahead-of-time batch-style compiler. I assume that this is for cases where your Lua-using program is fussy about how it takes your extensions (e.g., mods for proprietary games) and so you can't count on loading some hypothetical Fennel runtime library before loading any of your code.
NoahTheDuke | 6 hours ago
oh of course, thank you.
technomancy | 2 hours ago
We could of course add a "standard library" containing functions, but other people have already built non-standard "standard library" libs for Lua already, and theirs are quite good, so there wouldn't be much point in reinventing the wheel when it would sacrifice the zero-runtime property we currently have.
kalkin | 10 hours ago
I'm stealing the keyword
accumulate. That is a good word, because it describes the 80% of use cases ofreduce.cblake | 4 hours ago
"aggregate" is both shorter and less addition-tinged, but (against it), "aggregate" can be both a noun or verb.
In statistics things like min, max, sum, average, and higher moments are called "summary statistics". So, "summarize" is a not oft mentioned possibility, though that creates a slight ambiguity over incremental/one-pass summarization as is often meant by "reduce" and bulk/multi-pass summarization { e.g. first calculate
mean, thenmean (x_i - mean)^2}. This incremental-vs.bulk distinction is another aspect/view of the complexity others are observing here.icefox | 4 hours ago
Hmm, I feel like
summarize()is too long andsum()is very specific, but I wonder ifsum_with()would be a good name?veqq | 3 hours ago
Janet provides
accumulateandaccumulate2, added March 2020. It seems you addedaccumulateon 0.10.0 / 2021-08-07, so maybe Calvin suggested it?technomancy | 2 hours ago
By that point I think Calvin wasn't hanging out in our channel. But it's possible that a Janet user was in the chat and suggested it. On the other hand, even tho Smalltalk doesn't have it, I had a memory that it did based on a random IRC conversation from over a decade beforehand, and that might be more likely to be where it came from.
bitshift | 17 hours ago
Anecdote from Python-land:
reduceused to be a built-in function in Python 2, but in Python 3 it was relegated tofunctools. Why Guido decided to demote it:So Evan is not alone in noticing this!
Contrast with
map/filter, whose Pythonic equivalent is comprehensions. Anecdotally, those seem to spark joy more often. (I like them, at least.)technomancy | 15 hours ago
I've noticed the usefulness of
reduceis massively reduced when working with imperative data structures. The python explanation says it's most useful involving+or*which makes sense because in python numbers are stable values and data structures are not.rtpg | 15 hours ago
There's also the abstraction ceiling from
lambda.If you want anything semi-complicated you'll need a block instead of a `lambda. Now suddenly you're writing:
here's the alternative.
The extracted body is rarely that useful in itself. Meanwhile the alternative is easy to tweak. Things like skipping over elements become a
continuecall (reducercan of course no-op by returning the accumulator of course). You're just looking at the operation (no indirection due to method naming)You're right in that in most imperative systems it's trickier because ownership is unclear ("should I copy"?), among other things. I do think tho that in Rust in particular you can get away with it because of ownership tracking (and be quite performant as well?)
I would posit that accumulation as a pattern is simply solved decently in languages like Python in a lot of "canonical" cases and thus the common reduce patterns... become
sumif you like method overloading.bakkot | 16 hours ago
I am very fond of reduce but I agree with the anecdotal experience. The problem with very general tools is that they require you to see the world in terms of very general abstractions, and that's rarely actually necessary for most line-of-business stuff and so is a rarely exercised skill.
I've been on a long term quest to eliminate the major use cases from JavaScript - so far
Object.fromEntries(wasn't ever actually a good use case forreducebut people kept doing it anyway),Math.sumPrecise(was originally going to be justMath.sumbut we ended up with the high-precision version after committee, oh well), andIterator.prototype.join(not quite merged but probably this month); next up is probablyMath.argmaxalthough there's a lot of other things I want to get through first.anex9d | 16 hours ago
show off catamorphisms and paramorphisms and reducing a list wont seem so bad :P
jackdk | 17 hours ago
Caveat: My experience is mostly Haskell and while I'm sympathetic to lisps, I've done very little with them. I find the contract for
clojure.core.reducerather difficult to follow:reducewith two args, it behaves kinda likefoldl1 f coll:(f (f x1 x2) x3)etc., with the first elements of the collection being in the first applications tof.(f). (My read is that the library author tried to makereducetotal when used in two-arg form, at the cost of needingfto work with zero and two arguments, but IMHO it's really a precondition violation. I feel raising an error would be more appropriate.)fis never called.reducewith three args, it behaves kinda likefoldl f val coll:(((f val x1) x2) x3)etc. As with the two-arg form, earlier elements are applied first.val.The thing that seems most surprising a that
(reduce f z [1])is(f z 1)but(reduce f [1])is1; and that(reduce f [])is(f)but(reduce f z [])isz. The reader is forced to care about boundary conditions much more than when usingmapandfilter. It's clear why after you think about what each form ofreduceis doing, but I personally would have distinguished between possibly-empty and definitely-non-empty collections and providedreduceandreduce1functions in the stdlib.daveliepmann | 13 hours ago
Rich agrees with you. Emphasis mine:
From Inside Transducers (2014).
jackdk | 12 hours ago
Interesting citation, thank you.
So the function is expected to both serve as the combining
(<>)operator (akamappend) when called with two arguments, an as the identity (akamempty) for the combining function with no arguments? That sounds like it's being asked to serve as the dictionary for aMonoidinstance but instead of passing in an explicit interface (or having the compiler do it for you), the function is overloaded to handle both zero and two arguments, but it's okay to not provide the initial value in certain instances? I still feel some heebie-jeebies at that design decision.tsion | 10 hours ago
That's right. I imagine it's downstream of the old Lisp convention where
(+)yields 0,(*)yields 1, and so on.+and*support any number of arguments, with zero and two as special cases.jjuran | 16 hours ago
Is this a syntax issue? Curiously, the article doesn't give any code examples at all.
In my own language, Varyx, I made these infix operators with terse names:
map,ver, andper. Here are some real-world examples:The infix orientation was a deliberate choice to allow you to chain operations without nested function calls, with data flowing from left to right (in contrast to Perl's
mapandgrep, which have the data on the right). Complex chains can be stacked vertically:(Making each of these operator names exactly three letters was also intentional.)
tomsmeding | 10 hours ago
I gather from your second example that
perreturns some kind of nil value in case the collection is empty, and you can useorto supply a default value in this case. Is that accurate?What if you want to naturally return nil from the reduction for some reason, but you also want to handle empty collections somehow?
jjuran | 6 hours ago
If the incoming sequence is empty,
perevaluates to the empty list,(). Theoroperator has lower precedence and works exactly like you'd expect: If the entireA ver B per Cexpression is empty (due to an absence of non-blank lines), you get"()". (The resulting string is subsequently parsed as an expression.)Yup! Nailed it.
In that case, I might use ternary logic. The operators got delayed while I mulled syntax, but once they're implemented:
sequence ? sequence per {...} : alternative. Or if the sequence is a complex expression that you don't want to repeat,if let s = sequence then {s per {...}} else {alternative}.shonfeder | 16 hours ago
IMO, just a skill issue: any experienced programmer should have a sold grasp of folds, why are general, how to read and write code using them, since they are generally better than explicit recursion. But a lot of people don't learn the concepts essential to the trade.
manfred | 11 hours ago
Possibly, but it's an indication of the higher cognitive load in writing and understanding the code.
theblacklounge | 10 hours ago
Experienced programmers should use the most readable available function. In JavaScript, Python, and Swift that's hardly ever reduce.
coffee | 11 hours ago
I can only agree. I'm used to seeing and writing folds quite often, and I more often than not find them easier to comprehend and grasp the intent of than bare loops. It really depends on the paradigm one was socialized in.
rprospero | 8 hours ago
Though I'm sure that an outlier will respond to this comment, every coder that I've met who claims that folds are a code smell will also claim that explicit recursion is a code smell.
rr | 7 hours ago
What reading would you recommend if I were to try and introduce these concepts to other peers? I have a similar experience than OP’s.
sayyadirfanali | 11 hours ago
i think reduce is not as intuitive because the output which the structure "reduces to" can be any type in general. that's not the case with maps and filters, which always produce the same structure as output.
this makes reduce or fold a bit too general when we see it for first time.
also, with laziness, as in Haskell (foldr v foldl), it becomes extra confusing to reason operationally about.
hyperpape | 14 hours ago
I can no longer remember the names, but I distinctly recall one rust contributor observing that
sumexists because another early contributor really hatesreduce.I appreciate that person’s contribution.
frontsideair | 12 hours ago
I think part of the reason is that reduce is less ergonomic in languages without currying, partial application, and a lot of nice combinators lying around in general.
stathiss | 10 hours ago
I teach people Clojure, and
reduceis the first thing novices latch on when they start learning, mainly because it allows them to reproduce for-loop semantics that they are familiar with from imperative languages. I make them avoidreduceno matter what, in order to push them to use higher-level tools from the language and only use it when there is no other choice.sjamaan | 7 hours ago
That's interesting! I would've thought a for-loop can be more directly translated to a
doseqorloop/recur.liberty | 18 hours ago
I like reduce but I did have to learn it.
janiczek | 12 hours ago
This. Nowadays
List.foldl (\item acc -> ...) initAcc list(or Dict.foldl or what have you) are a second nature, but it definitely took some time to get used to it.A funny thing I like (in the Elm ecosystem) is that the order of arguments inside the reducing function is the same as The Elm Architecture's
updatefunction: the message coming in, then the model you're updating -update : Msg -> Model -> Model~foldFn : item -> acc -> acc.dmytrish | 9 hours ago
Rather confusingly, the accumulator argument comes first in Haskell (and I prefer this, because it mirrors the inputs) . One can get used to a specific implementation in a specific language, but for a more polyglot/all over the place enthusiasts like me, details like this are one of the reasons I dislike foldl/foldl'/foldr/foldl1 (especially in Haskell, where their interaction with laziness is so unpredictable).
Fold feels like a very powerful but a complex and leaky abstraction. Its natural use is limited to associative operations in pure code, and even there it still can be awkward in practice.
colonelpanic | 2 hours ago
This is terrible PR feedback and you should never leave it at that. This is a purely subjective complaint, maybe you're not good at reading?
The point of giving this feedback is not to dumb down perfectly reasonable code, but to tease out ambiguity. You need to formulate the concern in an objective way that specifies why it's hard to read. If you're unable to do that, then it truly might be a skill issue.
gunduzc | 14 hours ago
Interesting. As someone coming from the imperative world who's trying to adapt to functional programming, those three felt like a package deal to me.
david_chisnall | 10 hours ago
I think reduce is often exposed as an accumulate-like operation that takes one element at a time, which makes it hard. Most C/C++/similar-language programmers use a map-reduce model all of the time: compile is map (source-code file to object-code file), link is map (object-code files to program / shared library). But the reduce phase can't be implemented as an accumulate-like operation, it requires access to each of the objects simultaneously.
I complain about xmake a lot, but I use it because it's less annoyingly stupid than the alternatives. It expresses each high-level build task as a map step and a reduce step, though it calls them compile and link. For a language like Rust, the compile step might be a no-op and the reduce step do the compile-and-link together. For a language like TypeScript, compile might turn TypeScript to JavaScript and then link do nothing. It's a useful abstraction, but the link step isn't written as a lambda that is called once per output from the compile step, it's written as a lambda that takes the complete set of those files as an argument.
travisgriggs | 3 hours ago
This is a first attempt to give words to a notion that has been percolating in my mind for some time.
My programming pedigree is something like Fortran -> Smalltalk -> C -> Python -> Elixir with various polyglot side trips in other hybrid languages. Smalltalk had reduce (called inject: to complete the pentfecta of ..ect methods) and I reached for it less than map (collect) and filter (select). But I was more comfortable with it there than I have been in either Python or Elixir. Part of the reason I think, was that smalltalk has my favorite of the anonymous closure syntaxes. It was lightweight, consistent, and distinct. Other languages have multiple forms for closures and their arguments and are often not as distinguishable from other code flow bodies. So I think that can play a part.
In programming languages, we generally have two basic ways of encoding meaning around variables: by name/binding/keyword or by position. Array vs Dictionary/Map. Ordered function parameters vs named parameters. It has been my experience that position can be the more efficient/terse of the two, but it is the weaker of the two for maintain comprehension.
Reduce usually, but not always, encodes position 1 of the reducer function as the accumulator, and position 2 as the enumerated value. Elixir itself flips them though. If reduce is a “power tool” you get out when needed, one of the first things you have to do is remember which position is which. And since many reduce functions often accumulate the same type as they enumerate, compile time type checking won’t even help you. I find when I’m staring at any non trivial reduce function, that I essentially start thinking similar to when I’m reading assembler, “I’ve got this recursive descent like path, and what’s in what register again?”
In the case of Elixir, I can just make a recursive multi function which will give names much better than a genericsized function formatted in multiple levels of indented block structure. And as soon as the parameter position meanings get two complicated, I just switch to passing named structures. So in elixir, I’ve found that a bespoke reduction communicates better, often formats with really or even less lines of text, and surprisingly is even faster usually.
Yogthos | an hour ago
Reduce is basically an FP equivalent of using a generic loop. Typically, it's better to use a specific higher order function like
maporfilterwhich gives an indication of the intent. If I'm reading code and I seefilterthat gives me immediate context for what the code is meant to be doing. If I seereduce, I now have to walk through the body to infer the intent.mt | 14 hours ago
I've been catching up on Sean Parent's talks recently, and one of his big goals for programmers is "No Raw Loops". It comes from C++ world, but I think it's applicable further.
In that light,
reduceantipathy is understandable: you have to hold state in your head to ensure that some invariants aren't violated across iterations—in simple maps/filters you have to care about one element only, which shrinks state space considerably.So the goal (if we're stretching the No Raw Loops further) is to find the actual algorithm and use it—or write one and extract it (and then the implementation may use reduce, OK).
xavdid | 13 hours ago
Ironically, I gave this exact feedback to a more senior engineer during a code review many years ago. They wrote something using
reduceand I said it was too hard to understand. They were very nice about it, but I wonder if it shouldn't have just been a learning opportunity for me.In any case, I think it comes down to the function signature.
mapandfilterboth act on an array and return an array.reduceacts on an array and returns... any container? It's more powerful, but takes a lot more reading.I've been more comfortable using it in Typescript recently because the signature is generic so I can be explicit about the output type (and the compiler enforces it).
But I don't like that when returning the accumulator, the simplest thing to do (in JS) is:
Which modifies the original object. In practice this doesn't matter because I'm starting with a blank object. But it's still has the potential for unintended mutation, so the safest thing to do is:
But that makes a lot of copies, so it's probably less performant. So then I end up with this whole philosophical debate about how to write a simple function and then I wish I hadn't used reduce in the first place.
munificent | an hour ago
I believe part of the friction has to do with empty collections.
Running
maporfilteron an empty collection is fine and the resulting behavior is obvious: you just get another empty collection back.But with
reduce/fold, you actually need to think about it.In Dart, there are two separate methods:
fold()takes an initial value and successively combines it with the values in the sequence using the given combining callback. If the sequence is empty, you just get the initial value back. But in order to call this, you need to have some sort of meaningful initial value that makes sense.Then there is
reduce(). It only takes a combining callback. It uses the first value in the sequence as the initial value and the combines it with the remaining elements. If the sequence is empty, it's a runtime exception.Both are useful in different contexts, but you have to do some real thinking to know which one is the one you want.
map()andfilter()(where()in Dart), just don't have that edge case to worry about.isagalaev | 48 minutes ago
I concur, adding my anecdotes to the author's. One note is that it tends to depend on whether the reduce represents a readable high-level concept or is just a weird reformulation of a generic for-loop.
So if I see (in Python):
this is easily readable as "unionize all sets of tags". But if I see (in JavaScript):
then I don't see the point of being "more functional". (Even though I'm totally guilty of doing the latter in the past.)
coby | 12 hours ago
Anecdotally, can confirm. My colleague and teammate, a very capable staff developer, likes map and filter (in TypeScript) but says he tends to avoid reduce.
rs86 | 22 minutes ago
I like folds just fine. It’s soothing to find the empty/identity element and the operation that respects it. Is this a sign of AI turning us lazy?
zanlib | 9 hours ago
Quoting from a note I wrote when we still had juniors in this industry and I wanted them to have a quick reference when to use which array method in JS (especially using
reducein place offlatMap).I wrote these two tutorials mostly for my colleagues who were looking for a gentle introduction to FP patterns in JS, so they're a bit specific to that language, and I think they might read as too trivial to even be writing about, but they were useful to some people and I think demystified the way one can think about data structures in a functional way.
joelgrus | 8 hours ago
I once had a manager who loved to use reduce, and every single code review I would ask him to change it, until I finally broke him of the habit.
My primary complaint is that it (typically) saves like 3 lines of code but also makes the code much less readable and harder to reason about. That's a bad tradeoff!
(I finally convinced the aforementioned boss when I reviewed a PR and found a bug that was disguised by reduce but that would have been obvious if he'd just used a for loop.)
aiono | 5 hours ago
I think familiarity definitely plays a role. A typical developer is used to think in side effects, not with types. So when someone sees a function like reduce, they typically try to emulate it step by step, which feels like a more convoluted way of writing a for loop.
Also IMO higher order functions should have the function as the last parameter. Which might be the reason why it's worse in JavaScript and Python. Compare JavaScript:
and Python:
with Scala:
Function as a last argument lets you separate the iteration into a new line, making it look like a looping primitive familiar to the developers. I find it harder to see the array and the default value in the other versions. Whereas in the Scala one I can easily see them and then read the function.
masklinn | 4 hours ago
Having the function at the start provides useful support for partial application, which is why Haskell and OCaml do it that way. Python likely picked it up from there, it’s a lot less useful (
functools.partialexists but function composition is uncommon), and I think would be as useless the other way around due to the crippled lambdas andreducebeing a free function.icefox | 4 hours ago
Anecdotally, I like
reduce()/fold()just fine, but in Rust it's often easier to refactor/rereadfold()'s intoforloops when you have to change things.