I think this is because in an imperative language, `reduce` does not actually give you much over a `for item in collection` loop. With `map` and `filter`, you immediately learn something about the result (it's a list of the same length as the original, with each item only depending on the corresponding original item; it's a list containing some of the original elements unchanged and nothing else). This is useful, so `map` and `filter` are good.
With `reduce`, the result could be anything, and in an imperative language, side effects are also possible. So it's just a loop with worse syntax.
(Admittedly, in an imperative language, `map` and `filter` could also have side effects, though I think most people would consider this bad style.)
I think that in every imperative language that offers `map`, `filter`, `reduce`, or similar, the written contract of this API should state that any higher-order function handed to it as an argument must be free from side effects.
I think I’ve seen several language core APIs have this in their contract, e.g. `Stream#reduce` in Java [0] (emphasis mine):
> accumulator - an *associative, non-interfering, stateless* function for combining two values
I mean, most of the code that I write would be side-effect free anyway. In an imperative loop, this would also be true except for updating local variables. If this is the case, `reduce` really is the same as a loop over a collection, except that the names for the state passed between iterations come out better. In the `reduce` version, you can name the parameters to the reducer, but often not the return values. As a reader, one needs to connect the return values to the parameters by position.
(Note that by "loop over a collection", I explicitly mean a looping construct that gives the elements of the collection directly, instead of looping over indices and extracting the elements manually.)
In Rust these specifically take `FnMut`, a function which can update internal state, rather than `Fn` which can't easily. In `map`/`filter` you can't rely on the iteration order so that's rarely useful – maybe something 'logically' stateless but which needs a mutable connection/threadpool/cache/etc, or something like a counter which is really a second reduction. There's even `inspect` which is explicitly for side effects. In `fold`, the order is guaranteed and you could use it for a state machine, a fiddly `zip` with other mutable iterators, etc – something you need to perform the reduction, but which isn't really an output, I think you could reasonably write either
> I think that in every imperative language that offers `map`, `filter`, `reduce`, or similar, the written contract of this API should state that any higher-order function handed to it as an argument must be free from side effects.
Does the written contract for for-loops specify this as well? Because that's the obvious alternative for a reduce that an imperative programmer would reach for: a for-loop with the programmer managing the accumulation manually.
I also don't see why a reduce should be side-effect free. There's nothing wrong with having a type def't for reduce be
base.data.List.foldLeft : (b ->{e} a ->{e} b) -> b -> [a] ->{e} b
Where {e} indicates a side effect. Here, the reducer can have side effects, and those side effects propagate to the result of the fold/reduce.
I was going to write a question asking if reduce is the thing I know as accumulate (I think I picked this up from SICP). But then I went to wikipedia, and it seems that an even more common name is fold.
Here's a hypothesis: The fact that the same operation has half a dozen different names makes it sound like there is a lot to learn. If I am totally familiar with fold, and i come upon a reduce, I may need to think more about what's going on, which is distracting.
I don't think map and filter have so many synonyms? I know select for filter, but it seems to me less common.
And in some contexts you have the subtle distinction that fold is linear and reduce requires an associative operation and an identity element (aka a monoid)
I agree, but I think a lot of it is variable name abuse on the accumulator, making it unclear.
I've seen a lot of single letter or worse, a coworker who named it "cum" for short which is super not okay
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!
While map is a great name, I always struggle to remember if ‘filter’ keeps elements that match the condition or removes them.
I mean, it’s like a colander: you filter noodles and water, but which one do you keep? The noodles, right? But, replace noodles with tea and now you want to keep the water part.
Smalltalk has #reject: which does that. You could, of course, just wrap a not around the test in the closure, but sometimes reject with a well-named predicate is easier to read.
`remove-if-not` was deprecated before the Common Lisp standard was approved and yet it remained (and will never be removed because the standard will never be updated). That's not deprecated for any practical purpose. And it's more convenient than using `(remove-if (complement #'some-predicate) sequence)`
Scheme has `filter` and `filter-not` in the SRFI-1 list library. Both of which can easily be written using a fold to bring this vaguely on topic.
I agree that keep-if is a better name. But you reiterated my point: remove-if works in the counterintuitive way the original comment noted, i.e. not like the common connotation of 'filter.'
As for deprecation, IIRC the '-if-not' functions were deprecated because the committee felt the 'complement' function accomplished that task better.
Nonetheless, they were fooled by that function, because it's just keep-if by a funny name that includes "not" suggesting that it contains a complement that might be factored out.
If you want keep-if, you don't want to use a different function, which forces you to complement your predicate. If you want (keep-if #'redp jellybean-list) to keep the red jelly beans, you don't want to write (remove-if (complement #'not-red-p) jellybean-list). If keep-if has a silly name remoe-if-not, you might nonetheless prefer (remove-if-not #'redp jelly-bean-list).
Shims like complements are ugly, and compilers won't optimize through them for arbitrary function definitions (whose source code is not even in scope), so it is good to have both keepers and removers. Heck, it's useful to have a function which does both in one pass returning two values: the filtrate and the retentate.
Kotlin has filter and filterNot (it also has separate "reduce" and "fold" functions, dependingon whether you want to specify an initial accumulator value or not)
I was thinking an apt analogy might be making stock -- you filter out all the solid food you don't want to keep in the liquid.
And it's a doubly-good analogy, because I have occasionally gotten that confused in real-life as well. Twice in the past ten years I've had a stock boil away for three hours, and then set a colander in the sink and poured it through, only to watch my beautiful stock swirl down the drain because motor-memory made me forget that I wasn't draining pasta but should have put the colander in a bowl...
Talking about un-guessable, misleading function names,
C++ std::remove.
I would never have guessed what it does exactly. (It moves elements that match the filter to the front, and moves the end-marker forward. Leaves all the elements in the collection. You need to erase them yourself. )
There's always the Ruby strategy of just making all the names work. `select` and `filter` are buddies and you can use whichever you want or even go back and forth. Not a fan of `reduce`? That's fine, `inject` has got your back. Miss getting to type `collect` from Java or Rust? Don't worry, just use it instead of `map`, it's the same thing.
The filter keeps the tea... it's just that you then lift the filter out of the cup, carrying the tea with it. Flip your brain around to see it from that direction and it might help you with the mnemonics.
In elixir we have Enum.filter (run a predicate over the enumerable keeping the things that match the predicate) and Enum.reject (run a predicate over the enumerable removing the thing that match the predicate)
I think since we have a pair of them and reject is so obvious it helps me remember which way filter works.
I think Enum.keep and Enum.reject might be a better pair, but I've used them enough to internalize it now
In GNU Guile `reduce` is described as a special case of `fold`, where the first element is suitable to be used as initial value, while `fold` is more general and lets you specify another initial value. I think that makes a lot of sense.
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.
Reduces the list to another list three times as long.
It's a reduction in the sense of a transformation (also often seen in complexity theory), not in the "this makes this smaller" everyday usage that I think about first.
In rust iterators there's both fold (you supply the initial value) and reduce (it uses the first element as the initial value, doesn't work on empty iterators)
I've been writing Haskell for more than a decade and I can tell you in any situation whether I should use foldr or foldl' (right fold or left fold), but I still cannot remember the callback is supposed to acc elem or elem acc. I only remember that they are the opposite. I just consult GHCi every single time.
Similarly with scanl and scanr, which return each intermediate accumulator.
Human memory is weird. If you know you can get the answer from an external tool in two seconds, you do not remember it. But it is still annoying to spend that two seconds every time you use the function.
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 is more brainpower than map or filter. For this reason I have sometimes refactored code to use foldMap instead of foldr or foldl' so you no longer need to think of the direction of the fold or the order of arguments.
That's what I tend to do, but since foldr/foldl' is so ubiquitous in Haskell it would be nice if I could just remember the argument order of the callback. kccqzy's explanation might just help me do that :)
It's still a complex and more abstract function than map or filter. Those do a single thing that's easy to grasp. reduce/fold can be easily abused to duplicate the effect of most other collection functions, at the cost of making the code less readable. Although for slightly-too-clever people, that could mean you only need to know one function instead of all of them.
But it hurts readability. If you're going to do it, at least don't use it anonymously, but give it a name that clearly describes what's going on.
But even then, there can be hidden performance traps. I've often seen javascript that used reduce and created the new accumulator by using a spread on the old accumulator and adding the new one: `[...acc, newValue]`. But that spread is another iteration inside a loop, turning it from O(n) to O(n^2). A for loop where you append it is much faster.
When the accumulator isn't the second argument in a fold, left or right, it feels wrong and I waste some time cursing whomever made a silly mistake like getting the order wrong.
You want the accumulator second to match up with `cons` and similar functions that expect an initial/existing value second.
Luckily the functional languages I use the most are sane in that respect.
Ruby's `[1, 2, 3].inject(:+)` alias for `reduce` (name borrowed from Smalltalk) nicely reinforces that very mnemonic: you inject the operator _between_ the elements: 1 + 2 + 3.
:+ here is symbol meaning "send + message", more general form takes a block:
[1, 2, 3].inject { |a,b| a + b }
The downside of the "between" mnemonic is encouraging not handling an empty collection! Ruby's separate syntax to pass (args) {block} at least offers natural place to add initial value, but with symbol shorthand the API is harder to guess:
numbers.inject(0) { |a,b| a + b }
numbers.inject(0, :+)
(Also, it's always a left fold ((a) OP b) OP c, even if you give a symbol like :* which parses right-assiciative without parens — making the "inject operator between values" mental model less accurate.)
In some programming languages with RPN you can avoid this problem, because it makes sense to put it in the stack as the initial value, and then you can as easily have multiple initial values; and then the callback function can read that from the stack that you had put there, like anything else you will push into the stack to read it back later. For example, in PostScript you can write something like:
0 exch {add} forall
However, this is not as good if you want to use the first element as the initial value instead, but still it can be done but it is then not as simple (unlike in programming languages that do not use RPN but instead with function call with arguments, in which case it might be simpler).
I guess names as SELECT and WHERE are like SQL (although SQL works differently than other programming langauges).
> 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.
I think their point was that map/filter _individually_ look identical in most languages, i.e. map looks the same across most languages, and filter looks the same across languages, not that map and filter look identical to each other in most languages.
More trivially, map and filter are operations that can be understood by what they do to individual elements, while reduce is a folding operation that is applied recursively on its own output. Much harder to think about.
Speaking as someone who often tries to reduce my use of reduce by replacing it with map and filter where possible, for me, falling back to reduce is analogous to falling back to a while loop or a for loop: I avoid it if I can.
The problem with reduce is that it can do so much, and therefore it is less clear when reading it quickly what it might be doing.
It's simple really: looping is something we've all done a ton. Map is just a specialized version of something you do all the time, made better/simpler: what's not to like (and learn quickly)?
Reduces are used much, much less often. Most devs don't get familiar with them as a result, so every time they have to read a `reduce` they have to re-learn it. And of course, it's a much more involved/complex function, so that exacerbates it.
I dislike reduce because people sometimes do wild things in the callback that take a lot of mental effort to understand.
Sometimes people abuse .map as well to do things that are not obvious (i.e. instead of mapping elements of an array to another array, they modify global variables in a for-loop fashion, and discard the result).
But reduce is abused more often and you always need to think really hard if e.g. the initial accumulator is passed or not (it's optional in some languages!), if a correct one is passed (when a compound type is used) and so on.
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.
map() and reduce() are equivalent in terms of jargon, IMO. Map also suffers from name collision with dictionaries/objects/whatever your language wants to call a key-value pairing.
The fact that a word has a second sense does not negate the existence of the first sense. Especially not in computing where nearly all of the jargon is wildly polysemous.
In my experience, it depends a lot on the language and the folks you work with. I’ve gotten an eyebrow and a stern talking to for using ‘map’ in JavaScript once. Some people are die-hard about statements and keywords and imperative programming and their world view and be myopic.
“We can’t have map in our codebase, we need to be able to hire anyone off the street and have them comfortable in our codebase.”
Well… since when did we hire random people off the street?
I’m used to functional programming. For me, reduce is perfectly normal. Fewer intermediate variables. No pesky statements, just a nice expression. Great.
Buuuut… some languages think implementing tail call optimization is too hard or bad or for ivory tower academics. Or they’re dynamically typed. And then reduce does become difficult to special case and make performant. So even if you like the juice it’s probably not worth the squeeze.
It was a great time working with Haskell professionally. I didn’t have to constantly defend my style of programming! But in “everything” languages… well you do. Everyone has to agree on which subset to use. And programmers are like cats. Good luck getting them to agree on anything. Even once you agree there will always be that one challenging the decree.
(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.
IIRC both Dijkstra and Knuth used this approach extensively, instead of "if (some_cond) break;" — partly because the latter was not yet invented, partly because of their reliance on explicitly stated and documented right in the code loop invariants.
That's fair. Let me modify my statement: If the person doing this was over 50 and had developed this habit in old languages where that was basically idiomatic, it wouldn't scandalize me - though I'd raise an eyebrow to see it in a modern language.
But if someone's 28 and doing that stuff in Python or JavaScript, they're a bit of a loon, imho.
For me it's the name[0]. map puts out an array that has been mapped from another array. filter puts out an array that is a filter of the input array. both of those are always true. reduce, on the other hand, may put out a reduction of the input array (probably most of the time), but the fact that it may not means that what is happening is not actually a reduction. In languages like js/ts, you don't even have to return anything of the same type as the input array's elements. You could literally "reduce" and array of integers to a cancellation token, or a state object, or anything else.
I realize it's not the most efficient way to work, but I like my code to read like instructions. There's nothing reduce will do that a for loop won't accomplish and the for loop (+ an accumulator, of course) is more clearly "readable" than reduce. If I read map, I know what's going on. If I read filter, I know what's going on. If I read reduce, I have to figure out what's going on, even if I'm pretty sure what is going on. If I could rely on reduce to always give me back an element of the input array, I would use it more. But since it can give back anything, I prefer the simplicity of a for loop.
[0] I don't have any suggestions for "better" names because the whole operation is hard to sum up in a word? "dispatch" makes sense, as a function dispatching a function over each element in an array, but it masks the concept of accumulation from return values. "transform" is accurate, but hardly descriptive at all. the list goes on. It's an undeniably useful little function, it's just hard to make it easy to understand and therefore debug.
But I honestly very rarely use it (by either name) outside of a couple of pasted-in snippets (that I can't recall right now) where the strategy fits exceptionally well, probably because of the dumb reason that I tend to forget which block argument comes first (accumulator, or iterated item)! With other two-item argument lists such as `Hash#map` it being `key, value` makes sense, but with reduce/inject I don't see an obvious order. And I guess I learned before it was likely that some kind of AI autocomplete would be filling the args in for me.
It reduces n items to one item, recursively. The items don’t have to have the same type. Arguably accumulate is a more fitting name. I think of “reduce” as in cooking, boiling a volume of stuff down to some essence.
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.
Reduce always makes me question the performance and order of operations. The most I'll do in Python is like
sum(x[1] for x in args)
which is map + reduce. And that's only if x[1] is a number. That's about it. No equivalent in JS. Whenever some JS code has map, I'm like why, and rewrite it as a loop.
This is also assuming we're talking about regular code and not an actual map-reduce framework like Spark.
I prefer map+filter because unless performance demands it, things are easier to debug. I can vet the process of transformation (and its intermediate results) and then vet the process of exclusion.
reduce has complexity to handle the edge case of an empty iterable, and also for the case of a binary function with different types for inputs and outputs. That makes it harder to reason about and "uglier" than map and filter. People probably hate sum and product significantly less, both those also have the edge cases of empty iterable, in which case the natural result is 0 for sum and 1 for product sure, but of what type?
While we're on the subject, can someone explain to me why in Rust, you need to annotate the type when you call .sum() on an iterable? For example
let p: i32 = [1i32, 2, 3].iter().sum();
println!("hello {}", p);
That works, but fails if I replace `p: i32` with `p` or `p: i64`, and I cannot find a satisfactory answer in any thread or llm. The obvious question is why the compiler cannot infer the type from the element type of the container, and the naive response to that is for flexibility summing into a bigger type. But in that case, why would `p: i64` be rejected? And what other type is allowed besides i32?
166 comments
[ 7.1 ms ] story [ 239 ms ] threadWith `reduce`, the result could be anything, and in an imperative language, side effects are also possible. So it's just a loop with worse syntax.
(Admittedly, in an imperative language, `map` and `filter` could also have side effects, though I think most people would consider this bad style.)
I think I’ve seen several language core APIs have this in their contract, e.g. `Stream#reduce` in Java [0] (emphasis mine):
> accumulator - an *associative, non-interfering, stateless* function for combining two values
[0]: https://docs.oracle.com/javase/8/docs/api/java/util/stream/S...
(Note that by "loop over a collection", I explicitly mean a looping construct that gives the elements of the collection directly, instead of looping over indices and extracting the elements manually.)
Does the written contract for for-loops specify this as well? Because that's the obvious alternative for a reduce that an imperative programmer would reach for: a for-loop with the programmer managing the accumulation manually.
I also don't see why a reduce should be side-effect free. There's nothing wrong with having a type def't for reduce be
Where {e} indicates a side effect. Here, the reducer can have side effects, and those side effects propagate to the result of the fold/reduce.Here's a hypothesis: The fact that the same operation has half a dozen different names makes it sound like there is a lot to learn. If I am totally familiar with fold, and i come upon a reduce, I may need to think more about what's going on, which is distracting.
I don't think map and filter have so many synonyms? I know select for filter, but it seems to me less common.
But for unioning a bunch of spark dataframes together i think
is much nicer than People just get a bit funny, especially now you have to import it from functools- arg1, arg2 and return value are all of the same type e.g `ADD`, `MAX`, `CONCAT` etc
- and there is an identity value e.g zero for `ADD`, -math.inf for `MAX`
I recommend checking this article[1] on how monoids play nicely with reduce.
[1] https://fsharpforfunandprofit.com/posts/monoids-without-tear...
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!
While map is a great name, I always struggle to remember if ‘filter’ keeps elements that match the condition or removes them.
I mean, it’s like a colander: you filter noodles and water, but which one do you keep? The noodles, right? But, replace noodles with tea and now you want to keep the water part.
Naming is hard I guess.
bsnpApproved := tvShows reject: [ :eachShow | eachShow hasNaughtyContent ].
(It also has remove-if-not but that's deprecated and if you use it your code smells.)
Scheme has `filter` and `filter-not` in the SRFI-1 list library. Both of which can easily be written using a fold to bring this vaguely on topic.
As for deprecation, IIRC the '-if-not' functions were deprecated because the committee felt the 'complement' function accomplished that task better.
If you want keep-if, you don't want to use a different function, which forces you to complement your predicate. If you want (keep-if #'redp jellybean-list) to keep the red jelly beans, you don't want to write (remove-if (complement #'not-red-p) jellybean-list). If keep-if has a silly name remoe-if-not, you might nonetheless prefer (remove-if-not #'redp jelly-bean-list).
Shims like complements are ugly, and compilers won't optimize through them for arbitrary function definitions (whose source code is not even in scope), so it is good to have both keepers and removers. Heck, it's useful to have a function which does both in one pass returning two values: the filtrate and the retentate.
And it's a doubly-good analogy, because I have occasionally gotten that confused in real-life as well. Twice in the past ten years I've had a stock boil away for three hours, and then set a colander in the sink and poured it through, only to watch my beautiful stock swirl down the drain because motor-memory made me forget that I wasn't draining pasta but should have put the colander in a bowl...
C++ std::remove.
I would never have guessed what it does exactly. (It moves elements that match the filter to the front, and moves the end-marker forward. Leaves all the elements in the collection. You need to erase them yourself. )
`filter(where:)` like in swift...?
In Common Lisp both functions exist, under the names `remove-if` and `remove-if-not`.
I think since we have a pair of them and reject is so obvious it helps me remember which way filter works.
I think Enum.keep and Enum.reject might be a better pair, but I've used them enough to internalize it now
APIs should make sense inherently. An IDE can band-aid a bad design, but that doesn't make it a good design.
Bad example:
Reduces the list to another list three times as long.It's a reduction in the sense of a transformation (also often seen in complexity theory), not in the "this makes this smaller" everyday usage that I think about first.
In rust iterators there's both fold (you supply the initial value) and reduce (it uses the first element as the initial value, doesn't work on empty iterators)
https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
Similarly with scanl and scanr, which return each intermediate accumulator.
Human memory is weird. If you know you can get the answer from an external tool in two seconds, you do not remember it. But it is still annoying to spend that two seconds every time you use the function.
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 is more brainpower than map or filter. For this reason I have sometimes refactored code to use foldMap instead of foldr or foldl' so you no longer need to think of the direction of the fold or the order of arguments.
https://arialdomartini.github.io/fold-mnemonics
But it hurts readability. If you're going to do it, at least don't use it anonymously, but give it a name that clearly describes what's going on.
But even then, there can be hidden performance traps. I've often seen javascript that used reduce and created the new accumulator by using a spread on the old accumulator and adding the new one: `[...acc, newValue]`. But that spread is another iteration inside a loop, turning it from O(n) to O(n^2). A for loop where you append it is much faster.
You want the accumulator second to match up with `cons` and similar functions that expect an initial/existing value second.
Luckily the functional languages I use the most are sane in that respect.
and in many case the accumulator is a tuple, and in many cases you need to know the length of the collection ( like average)
all in all, it’s a lot just to avoid a for loop.
I guess names as SELECT and WHERE are like SQL (although SQL works differently than other programming langauges).
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?
The problem with reduce is that it can do so much, and therefore it is less clear when reading it quickly what it might be doing.
Reduces are used much, much less often. Most devs don't get familiar with them as a result, so every time they have to read a `reduce` they have to re-learn it. And of course, it's a much more involved/complex function, so that exacerbates it.
So I love reduce, and have for many years.
Sometimes people abuse .map as well to do things that are not obvious (i.e. instead of mapping elements of an array to another array, they modify global variables in a for-loop fashion, and discard the result).
But reduce is abused more often and you always need to think really hard if e.g. the initial accumulator is passed or not (it's optional in some languages!), if a correct one is passed (when a compound type is used) and so on.
And even when they don't, you have to spend effort to determine that they aren't.
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.
If there's no standard function for it, it's trivial to write a utility function.
And as part of writing the function, give it a good name and think a bit about the order of operations?
So I think reduce() is just unnecessarily generic, unless it's part of a more complicated system like running a map-reduce.
While not as functionally pure, I always appreciate the Ruby each_with_object https://ruby-doc.org/3.4.1/Enumerable.html#method-i-each_wit... as a more pleasant interface for it.
“We can’t have map in our codebase, we need to be able to hire anyone off the street and have them comfortable in our codebase.”
Well… since when did we hire random people off the street?
I’m used to functional programming. For me, reduce is perfectly normal. Fewer intermediate variables. No pesky statements, just a nice expression. Great.
Buuuut… some languages think implementing tail call optimization is too hard or bad or for ivory tower academics. Or they’re dynamically typed. And then reduce does become difficult to special case and make performant. So even if you like the juice it’s probably not worth the squeeze.
It was a great time working with Haskell professionally. I didn’t have to constantly defend my style of programming! But in “everything” languages… well you do. Everyone has to agree on which subset to use. And programmers are like cats. Good luck getting them to agree on anything. Even once you agree there will always be that one challenging the decree.
I use both, but do not like reduce at all. It's harder to read, yes. But I see the point of using them all.
I really like taking the implementation away from the call site, so that the call site reads
(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.
Wow. That is the kind of monkey business that would have me running for the exits. Yikes.
But if someone's 28 and doing that stuff in Python or JavaScript, they're a bit of a loon, imho.
I agree, but I think that’s kind of the objection. It can be tempting to write your code as little brainteasers but…
I realize it's not the most efficient way to work, but I like my code to read like instructions. There's nothing reduce will do that a for loop won't accomplish and the for loop (+ an accumulator, of course) is more clearly "readable" than reduce. If I read map, I know what's going on. If I read filter, I know what's going on. If I read reduce, I have to figure out what's going on, even if I'm pretty sure what is going on. If I could rely on reduce to always give me back an element of the input array, I would use it more. But since it can give back anything, I prefer the simplicity of a for loop.
[0] I don't have any suggestions for "better" names because the whole operation is hard to sum up in a word? "dispatch" makes sense, as a function dispatching a function over each element in an array, but it masks the concept of accumulation from return values. "transform" is accurate, but hardly descriptive at all. the list goes on. It's an undeniably useful little function, it's just hard to make it easy to understand and therefore debug.
collection inject: aValue into: aBlock
#(1 2 3 4 5 6 7) inject: 10 into: [ :sum :each | sum + each squared ].
or from your example:
someHash := myArray inject: (Dictionary new) into: [ :accumulator :item | ... ].
The way I remember the order is it reflects the assignment you'd do is a while loop, sum := sum + each.
'for' loop is mutating.
Using 'reduce', you can do the same functionally. In some (somewhat) purely functional languages, there is no choice.
I also agree that a for loop is often clearer.
This is what reduce does, though? It reduces a list to a single thing. It seems like you're thinking of filter.
combine, accumulate it aggregate would have way more use.
This is also assuming we're talking about regular code and not an actual map-reduce framework like Spark.
I prefer map+filter because unless performance demands it, things are easier to debug. I can vet the process of transformation (and its intermediate results) and then vet the process of exclusion.
[0] https://en.wikipedia.org/wiki/Law_of_triviality
[1] https://www.smbc-comics.com/comic/noun
While we're on the subject, can someone explain to me why in Rust, you need to annotate the type when you call .sum() on an iterable? For example
That works, but fails if I replace `p: i32` with `p` or `p: i64`, and I cannot find a satisfactory answer in any thread or llm. The obvious question is why the compiler cannot infer the type from the element type of the container, and the naive response to that is for flexibility summing into a bigger type. But in that case, why would `p: i64` be rejected? And what other type is allowed besides i32?