13 comments

[ 0.17 ms ] story [ 32.9 ms ] thread
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

[0]: https://docs.oracle.com/javase/8/docs/api/java/util/stream/S...

Even though it makes print debugging harder, I think it would be better if the language enforced such a contract.
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

    .fold(init, move |acc, x| …)  // or
    .fold((state, init), |(state, acc), x| …).1
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)
For can have another set of variables in the header too. You can simulate it more readably even if you need to call the lambda.
Ive only used reduce at work half a dozen times and it does raise an eyebrow each time.

But for unioning a bunch of spark dataframes together i think

    df = reduce(DataFrame.union, list_of_dfs) 
is much nicer than

    df, *rest = list_of_dfs

    for other in rest:
        df = df.union(other)
People just get a bit funny, especially now you have to import it from functools
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
I find `reduce` useful for operations where:

- 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...

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!