Hacker News new | past | comments | ask | show | jobs | submit
Map and filter usually have only one arg and if they have 2, the 2nd is almost always a 0-based index. They look identical in most languages, even when Microsoft chooses to call them Select and Where.

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

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

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

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

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

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

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.

Since you mention mnemonics, here are the mnemonics I use to remember the (symmetrical) differences between foldr and foldl

https://arialdomartini.github.io/fold-mnemonics

In other words, look at the types. The type of the folding function (the first argument) indicates how each fold works.

  foldl' :: Foldable t => (b -> a -> b) -> b -> t a -> b

  foldr  :: Foldable t => (a -> b -> b) -> b -> t a -> b
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 (in particular "it replaces the comma") might just help me do that :)
I admit that I have always looked at an explanation like yours with x1,...,xn when using fold because I could never keep it straight in my mind.
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.
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).

Also reduce is a weird name.
What about fold?

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

Why? It does, after all, reduce a collection to a single value.
loading story #49737388
loading story #49737241
I suppose it's just so ... reductive, you know?
In the book "Simply Scheme", map is "every", filter is "keep", and reduce is "accumulate".
> … to call them Select and Where.

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.

loading story #49735658
loading story #49734276
loading story #49733682
loading story #49735835
loading story #49735392
loading story #49734310
Maybe those two could be filter_for (the “where” case) and filter_out.
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)
If you're making tea with a colander something is very wrong ;)
loading story #49735591
depends on the size of the sieve, but sometimes one does cook a whole stewpot of tea at once (f.e. in canteen)
> While map is a great name, I always struggle to remember if ‘filter’ keeps elements that match the condition or removes them.

In Common Lisp both functions exist, under the names `remove-if` and `remove-if-not`.

loading story #49734719
{"deleted":true,"id":49735975,"parent":49707608,"time":1789614683,"type":"comment"}
loading story #49735635
> Map and filter usually have only one arg and if they have 2, the 2nd is almost always a 0-based index. They look identical in most languages, even when Microsoft chooses to call them Select and Where.

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

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

They look nothing alike?

Exactly, and sometimes you also get the indez as argument of the function. `reduce(acc,(acc,elem,idx)=>…)`

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.

An IDE can fix that
Meh, if you need a computer program to understand an API its a bad api.

APIs should make sense inherently. An IDE can band-aid a bad design, but that doesn't make it a good design.

loading story #49734839
loading story #49736580