Reduce is a good illustration of the principle of least power[1]: it's powerful, flexible, general and low-level and can technically achieve any combination of summation, map, filter, find/includes, some/any/every, etc. But reduce is misused if it's reimplementing patterns available in higher-level form.
In cases when reduce is required because (for example) JS doesn't have a sum function, it should be kept simple. `arr.reduce((acc, el) => el + acc, 0)` is acceptable if lodash _.sum() is not available.
In cases when reduce is required because the higher-level operations like map/filter aren't flexible enough, decompose the reduction operation into simpler steps and use map/filter with multiple passes, or write a traditional for..of loop.
This principle also explains why enhanced/range/of loops are preferred over counter-based `for` loops, and counter-based loops over `while`. Technically all loops can be handled by `while`, but it's seldom needed because enhanced loops handle the common case with the cleanest syntax. Reduce/while/counter-based `for` loops are antipatterns where higher-level, less powerful abstractions exists.
arr.filter(e => e % 2 === 0).map(e => e * 2)
The only advantage of the reduction as I see it is performance, but this is highly dubious and would need to be profiled for proof (I don't recall seeing removing a pass like this matter in practice). And if perf does matter, a for..of loop would be clearer and one-pass, not to mention async-compatible:
const result = [];
for (const el of arr) {
if (el % 2 === 0) {
result.push(el * 2);
}
}
Exercises like this illustrate why verbal technical job interviews are useful in the age of LLMs--a series of A/B taste preferences seems high signal and ripe for discussion: "Ah, so you're choosing reduce for perf... please describe a scenario you encountered where this made a measurable impact".
In my case, it's more of a personal preference than any measurable impact. Practically, `for...of` would be the best approach. But I like to avoid chaining and avoid manually building an array (in your example I'd replace `acc.push(el * 2);` with `return acc.concat(el * 2);` -- even though I suspect `push` might be more performant but not sure). Why do I prefer it? Can't explain. If the nanoseconds really mattered then I might change it. Otherwise it's just one of the many ways of achieving the same goal.
Chained filter and map don't necessarily iterate multiple times. They certainly can but depending on how things are built they very often run as a single loop with the operations chained.
For me it's about the speed of understanding what the code does. Because filter and map are very constrained in what they can do they quickly tell me a lot about the shape of the computation I'm working with. In contrast reduce is much more flexible and so I need to do a much more detailed analysis to just answer a basic question like "is the result a scalar or another collection?".
ggorlen · · focus · HN ↗
In cases when reduce is required because (for example) JS doesn't have a sum function, it should be kept simple. `arr.reduce((acc, el) => el + acc, 0)` is acceptable if lodash _.sum() is not available.
In cases when reduce is required because the higher-level operations like map/filter aren't flexible enough, decompose the reduction operation into simpler steps and use map/filter with multiple passes, or write a traditional for..of loop.
This principle also explains why enhanced/range/of loops are preferred over counter-based `for` loops, and counter-based loops over `while`. Technically all loops can be handled by `while`, but it's seldom needed because enhanced loops handle the common case with the cleanest syntax. Reduce/while/counter-based `for` loops are antipatterns where higher-level, less powerful abstractions exists.
[1]: <a href="https://wiki.c2.com/?PrincipleOfLeastPower" rel="nofollow">https://wiki.c2.com/?PrincipleOfLeastPower
marcta · · focus · HN ↗
FlameWolf · · focus · HN ↗
ggorlen · · focus · HN ↗
FlameWolf · · focus · HN ↗
QuercusMax · · focus · HN ↗
ema · · focus · HN ↗