is not only unoptimized in the sense that it does O(n) age increments, but also does O(n) object clones (which are far more expensive). It also does twice as many array allocation as the naive procedural approach:
const ps = [];
for (const p of persons) {
if (p.age > 29) ps.push(p);
if (ps.length === 5) break;
}
If one wants to argue about readability, consider that the lazy approach requires understanding the semantics of both `chain` and `take` (in addition to all the mentioned downsides about extra code), whereas pretty much everything in the procedural approach can be found in a beginners JS course (or most other mainstream languages). And for an advanced developer's eye, the cost of the snippet is explicit.
Takeaway: sometimes, the seemingly overly simplistic solution is the best choice, and conversely an overly complex solution is a product of a semi-irrational bias, rather than objective analysis.
Assuming we want `canDrink` to exist in those objects for later use, reversing the steps here would mean duplicating the logic and the work of computing it across the map() and the filter(), instead of keeping those concerns separate and re-using values:
but map also creates a new array, and within map you are not mutating the items of the old array but creating brand new ones no?
[...].map(p => ({...p, canDrink}))
^ the original items of the array are not mutated in this map callback. you could mutate them if you really wanted, i.e.
I don't see how lodash could possibly filter a list of 30 by a criteria without visiting each item at least once to take a look at it. the only way to make it more efficient would be to have lodash collect the desired transformations (filter/map) and then run a single loop over the items, mapping only those items that meet the filter criteria. But that is not much more efficient than the [].filter().map() that you could do with native methods.
It doesn't have to visit all 30 items, just enough items to take 5 from, could be 8, could be 10 etc. The lazy approach _is_ to collect all transformations and apply them in the end.
Comments
In the article when comparing the native vs lodash implementation the native one is given as:
persons .map(p => ({ ...p, age: p.age + 1 })) .filter(p => p.age >= 30) .slice(0, 5);
^ But this is intentionally(?) unoptimized. You would instead put the filter and slice first, i.e.
person.filter(p => p.age >= 29) .slice(0,5) .map(p => ({...p, age: p.age + 1})
I imagine the example is artificially constructed to fit the flow of the narrative.
Consider that the initial idiom
is not only unoptimized in the sense that it does O(n) age increments, but also does O(n) object clones (which are far more expensive). It also does twice as many array allocation as the naive procedural approach: If one wants to argue about readability, consider that the lazy approach requires understanding the semantics of both `chain` and `take` (in addition to all the mentioned downsides about extra code), whereas pretty much everything in the procedural approach can be found in a beginners JS course (or most other mainstream languages). And for an advanced developer's eye, the cost of the snippet is explicit.Takeaway: sometimes, the seemingly overly simplistic solution is the best choice, and conversely an overly complex solution is a product of a semi-irrational bias, rather than objective analysis.
It's a slightly contrived example, but it demonstrates a real situation that can come up. Suppose instead it was:
Assuming we want `canDrink` to exist in those objects for later use, reversing the steps here would mean duplicating the logic and the work of computing it across the map() and the filter(), instead of keeping those concerns separate and re-using values: You could of course share the business logic by abstracting it into a function: But you'd still be doing the work twice. In this example it's trivial, but in a real-world scenario it might not be.if you are using slice(0,5) then all the extra objects you mapped are lost and you have no reference to them.
Not true. slice() clones-and-drops the array, but not the objects inside.
but map also creates a new array, and within map you are not mutating the items of the old array but creating brand new ones no? [...].map(p => ({...p, canDrink}))
^ the original items of the array are not mutated in this map callback. you could mutate them if you really wanted, i.e.
[...].map(p => {p.canDrink = p.age > 20; return p})
but that is a straight up hack.
Yes, but this will still filter through all the items, instead of stopping at 5.
I don't see how lodash could possibly filter a list of 30 by a criteria without visiting each item at least once to take a look at it. the only way to make it more efficient would be to have lodash collect the desired transformations (filter/map) and then run a single loop over the items, mapping only those items that meet the filter criteria. But that is not much more efficient than the [].filter().map() that you could do with native methods.
It doesn't have to visit all 30 items, just enough items to take 5 from, could be 8, could be 10 etc. The lazy approach _is_ to collect all transformations and apply them in the end.
ah, yeah you are right.
You’re overthinking it. And your version does 30 filters instead of 7 anyway.