7 ms·
How to use array reduce for more than just numbers
- nkrisc 7y agoReader mode is a huge UX upgrade here. Recommend it.
- yankjenets 7y agoOptimize for readability above everything else. Almost all of these examples I'd argue would be better suited for something like .map(). Little point in being overly cute, and if you are working with arrays large enough such that doing multiple passes is that much of a performance hit client-side, you have other issues going on.
- jrimbault 7y agoThat's also my minor nitpick, I feel like the syntax : `list.filter(myFilter).map(transform)` is clear about what I do and, each functions has a clear goal. Otherwise I enjoyed reading this.
- mikeryan 7y agoWhile your example reads a bit better, you’re now iterating through your list twice.
- fwip 7y agoMost code isn't performance-critical. This pattern will probably be dominated by the time taken to filter and map, not loop iteration.
- londt8 7y agoTechnically there is no iteration in filter/map/reduce at all.
- Stratoscope 7y agoThat's incorrect. All of those functions are implemented using iteration. Consider this excerpt from the .map() polyfill on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#Polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... // 7. Let k be 0 k = 0; // 8. Repeat, while k < len while (k < len) { // [loop body omitted] // d. Increase k by 1. k++; }
- saagarjha 7y agoDoes JavaScript have adapters to covert lists into generators or lazy iterators?
- hn_throwaway_99 7y agoUnfortunately not in a way that can optimize these methods. This is one reason the Java Streams API, which has a similar purpose (e.g. map, filter, flatMap, reduce, etc.), is a better design IMO because you can chain together method calls without creating (and iterating) the full array at each step.
- _bxg1 7y agoI think the intent was simply to teach. Obviously if something can be done with map() instead of reduce(), it'll be more readable as map(). But seeing how the other list functions can be made out of reduce() helps you understand how it works.
- azangru 7y ago> Almost all of these examples I'd argue would be better suited for something like .map() Roughly half of his examples take an array and return either a differently-sized array, or a different data structure altogether. I can’t imagine how one could argue for a map in these cases.
- noir_lord 7y agoIndeed, reduce is the swiss army knife since you can use it (sometimes when you shouldn't) to implement most of the other methods of map/filter etc.
- hn_throwaway_99 7y agoUse the right tool for the job. Array to potentially different sized array? Use flatMap (which he implements in his example, albeit not it an optimal way). Array to single object or value? Use reduce.
- spoiler 7y agoI'm not opposed to using `reduce` where it solves a problem in a readable or performant way, but it seems there's a trend among JS developers recently (I noticed this in the react ecosystem) to use `map` / `reduce` for _everything_. Sure, _some_ problems can be solved elegantly and efficiently with it, but a lost of the time it just impedes readability, and maintainability of the code. A `for of` loop (or your C-styled for loop) would sometimes be better suited.
- Waterluvian 7y agoI think it's perfectly fine in many cases. Like say writing a listToKeyed function. Where I find it fails is when there's a ton of imperative code that's really hard to understand the premise. The beauty of functional programming is that you can take these stanzas and break them into nicely testable, named functions.
- minitech 7y agoWhat’s a listToKeyed function?
- spoiler 7y agoI think they mean something that turns a list (array) of unique elements into a keyed collection (like a map or an object). So something like: [ {id: 1, name: 'Jane'}, {id: 2, name: 'John'} ] becomes { 1: {id: 1, name: 'Jane'}, 2: {id: 2, name: 'John'} }
- imtringued 7y agoThis is driving me mad. This operation is called groupBy and it is a solved problem. How on earth is HN oblivious to such a fundamental operation?
- minitech 7y agoWho’s HN here? What’s driving you mad? Not really clear what your point is.
- Waterluvian 7y agoIt's funny to see other perspectives. I've never used array reduce for numbers.
- kace91 7y agoAt my current project we try to do everything as functional as possible (we're backend developers using node). After a year or so of trial and error, we have pretty much stopped using reduce. The reason is that it pretty much always causes the code to be far less readable, specially for new devs that come from more object oriented approaches. With the .Map, .foreach and .filter, methods we don't usually have that problem, people get the hang of them quite quickly and find them more elegant, but reduce always seem to require to stop for a few seconds to learn what it's doing.
- azangru 7y ago> specially for new devs that come from more object oriented approaches Could this be solved by accepting in the team only those devs who are comfortable with functional programming? :-)
- kace91 7y agoIt could, but it's not in our hands. And to be fair to management, there's not much of an argument to be made to shrink the pool of candidates so we can use reduce, if we're not seeing that feature bring anything to the table... We already work with a somewhat small pool of candidates - they need to be somewhat competent, preferably familiar with niche technologies that we use in other areas, and willing to work mainly with JS.
- azangru 7y ago> there's not much of an argument to be made to shrink the pool of candidates so we can use reduce Sure, but since you mentioned that your team tries to write javascript in a functional paradigm, doesn’t it make sense to filter out candidates who aren’t comfortable with functional programming in general (and reduce is a marker, albeit very superficial, of candidate’s familiarity with this paradigm)? Of course I don’t know how hardcore your functional style is; whether it’s just map-filter-reduce, or whether it’s also currying, composing and lenses (basically, ramda; also not very familiar to OOP programmers, I would imagine), or whether it’s all the way down to imitating Haskell in Javascript (Maybe monad, Either monad, State monad, reader monad, lifting, etc.; basically, crocks).
- c-smile 7y agoThis sample function keyByUsernameReducer(acc, person) { return {...acc, [person.username]: person}; } const peopleObj = peopleArr.reduce(keyByUsernameReducer, {}); has O(n*n) complexity and memory consumption of the same magnitude. This, while being not so functional pure, is better I think: function keyByUsernameReducer(acc, person) { acc[person.username] = person; return acc; } But still... needless function call on each iteration.
- Rockslide 7y agoI too prefer to use the "inefficient" notation because it just feels wrong (kind of an antipattern in the functional sense if you will) to mutate the argument. And as long as you are not dealing with a huge input list, this doesn't really matter at all. Also, why would the memory complexity be n * n? Sure, a new object is created in each iteration, but its not like you have to keep the previous ones in memory - all but the last one can be garbage collected.
- minitech 7y agoIt’s n^2 time, not space. Each property of the object gets copied each time around, and the number of properties being copied increases each time around. It’s probably a good idea to start considering this kind of copying an antipattern. The good news is that the correct implementation doesn’t have to involve mutating any arguments, just a local with unambiguous ownership: const keyBy = (iterable, key) => { const map = new Map(); for (const value of iterable) { map.set(key(value), value); } return map; }; const peopleObj = keyBy(peopleArr, person => person.username); If you have an array, you even get to keep using array methods, which everyone loves: const keyBy = (array, key) => new Map( array.map(value => [key(value), value]) );
- c-smile 7y ago"new object is created in each iteration" Not just created, but properties are being populated one by one. "can be garbage collected" And what do you think is computational complexity of garbage collector? Yes, memory allocation in JS (GCable environment) is cheap. But garbage collection per se is not, it is at least O(N) complex.
- minitech 7y agofunction splitLineReducer(acc, line) { return acc.concat(line.split(/,/g)); } const investigators = fileLines.reduce(splitLineReducer, []); That’s unnecessarily quadratic. So yes, definitely use the more readable `flatMap` (with appropriate polyfill): const investigators = fileLines.flatMap(line => line.split(',')); (The `flatMap` in the article is not an appropriate polyfill) The object spread example has the same problem, like c-smile mentioned. Use a Map for lookups instead. > Some readers might point out that we could get a performance gain by mutating the accumulator. That is, we could change the object, instead of using the spread operator to create a new object every time. I code it this way because I want to keep in the habit of avoiding mutation. If it proved to be an actual bottleneck in production code, then I would change it. ↩ Avoiding (very local) mutation because it’s an FP bad word in a language like JavaScript is just a terrible way to go about things. Also, this turns out to be an actual bottleneck way more often than people notice.
- saagarjha 7y agoIndeed, some languages (such as Swift) provide a mutating reduce because it makes sense to use it in some cases.
- nirvdrum 7y ago> Also, this turns out to be an actual bottleneck way more often than people notice. I wish more devs -- particularly web front-end devs -- tried out their work on something that isn't a top-of-the line developer computer. There are plenty of devices out there that are computationally constrained and/or memory-bounded. Moreover, no one ever runs these things in isolation. So, while profiling in isolation makes a ton of sense to rule out external factors, in the real world you're competing with all the other open tabs and windows. That's a tricky problem to model, of course, but it doesn't mean we shouldn't try.
- d0m 7y agodeploying the same javascript code on both web and old android devices (using RN) keeps you honest about performance :O
- 7y ago
- rdtsc 7y agoReduce is also known as "fold" in other languages. https://en.wikipedia.org/wiki/Fold_(higher-order_function) https://en.wikipedia.org/wiki/Fold_(higher-order_function) And especially when using immutable data it can be used for iterating, traversal (lists and trees, like say an in-order fold), filtering and other things. But there it is not an just an nice trick like for JS, but it is the canonical way of doing things.
- saagarjha 7y agoReduce is slightly different from fold because it does not use an initial value, which means its semantics differ somewhat with regards to restrictions on types.
- hnra 7y agoReduce in Javascript has the initial value as an optional parameter.
- saagarjha 7y agoAnd it doesn't do types, so that's not really an issue either…
- johnday 7y agoFold does not always have an initial value either. The Wikipedia article linked in the post to which you responded says as much.
- saagarjha 7y agoSorry, I worded that poorly: fold usually does not have an initial value.
- sonnyblarney 7y agoI see a lot of for loops. Working on a project in which there are really nary such resources available, and I'm stuck with only 'for' ... guess what? I don't miss much of these functions; I just don't find myself reaching for them, wishing I had them. Either a for loop, or a couple of lines of code, or gasp you end up writing a short custom utility function to do specifically what you want, which is generally better optimized anyhow.
- adminu 7y agoUsually it is the other way round: you miss what you had somewhere else and learned to love. When you go back it hurts. Try a 800x600 monitor nowadays and you will cringe. For me after using functional constructs a lot I find myself wishing for them in languages where they are not available that easily.
- sonnyblarney 7y agoYes, we miss nice screens when we go back to crappy ones, agreed, but this is my point: I don't miss most of these lodashy functions when they're not there. Maybe a little bit.
- agumonkey 7y ago<deleted useless comment>
- dang 7y agoPlease don't do this here.
- jpochtar 7y agoreduce() is how you get imperative programming in a functional setting. JS is a perfectly good imperative language; for all these examples, you may as well use a for loop. Personally I prefer a functional style, but if you're going to use an imperative style, just use JS's built-in imperative for loop and assignment operators.
- galaxyLogic 7y agoWhen EcmaSCript 2019 hits the shelves it will have flatMap(). flatMap is the "corner-stone of monads" (poetically speaking) at which point monads will lose much of their mystique and things will be much advanced in the programmer land. You can use monads where you need them. http://2ality.com/2018/02/ecmascript-2019.html http://2ality.com/2018/02/ecmascript-2019.html More on ES2019 at https://medium.com/@selvaganesh93/javascript-whats-new-in-ecmascript-2019-es2019-es10-35210c6e7f4b https://medium.com/@selvaganesh93/javascript-whats-new-in-ec...
- ggregoire 7y agoTo balance all the negative comments about readability, I do think .reduce is often more readable than its traditional alternatives. Simply because its use cases are always the same so you can guess the code without reading it: * Reducing an array of numbers, with a number as initial value? => We are going to do some operations on those numbers. * Reducing an array of objects, with a number as initial value? => We are going to do some operations on one or several props of those objects. * Reducing an array of objects, with an empty object as initial value? => We are going to create a key/value object with computed key names. Then just by reading the name of the variable on the left of the assignment, you know exactly what's going on. And if you don't need to know about the details, you can just jump to the next statement. const products = [product1, product2, product3] const sumPrices = products.reduce(/* I don't need to read this */, 0) const users = [users1, users2, users3] const usersByFirstname = users.reduce(/* I don't need to read this */, {}) const usersByLastname = users.reduce(/* I don't need to read this */, {})
- eptcyka 7y agoSomewhat off-topic but the font is about as offensive to my eyes as a tire fire is to my nose. https://imgur.com/qrcE9vd.png https://imgur.com/qrcE9vd.png
- dang 7y agoOk, but please don't post unsubstantive comments here.
- codr7 7y agoWhile we're on the subject of reduction, I really think transducers [0] deserve a mention. The general idea is to stack operations and use reduce to perform the entire transformation in one go, as opposed to chaining calls to map/filter etc. Which means first class transformation pipelines and less wasted effort among other things. [0] https://clojure.org/reference/transducers https://clojure.org/reference/transducers
- imtringued 7y agoI seriously hope nobody takes the advice in this article seriously. Here are the conclusions you should take away from the article: Don't use reduce to reimplement map. Don't use reduce to reimplement flatmap. Don't use reduce to reimplement groupBy. Don't use reduce to reimplement filter. The minMaxReducer is the only legitimate use of reduce in the entire article. The above functions require less boilerplate code. With reduce you will have to write additional bookkeeping code for the accumulator and without mutable data structures [!] it would be incredibly inefficient. [!] We are far away from functional programming at this point.