Live data from Hacker News

Lodash's Chain vs. Native Methods

maartenhus.nl

11–20 of 25 posts

Re: Lodash's Chain vs. Native Methods

#11
post #5

Earlier quoted context omitted.

Yeah, years ago I got deep into this stuff and managed to implement a lot of it for fun, thinking I was doing something original, before discovering it’s not original (in my case, discovering Seq in OCaml). I think treating everything as lazy and potential infinite makes the implementation of operations much simpler than this article’s implementation.

I think for most usage, you’re right. There are places where it probably has more overhead than you’d want in a tight loop, and you’d be better off with the “bad” imperative version. E.g. “If a tree falls in the woods, does it make a sound? If a pure function mutates some local data in order to produce an immutable return value, is that ok?” I’m coming from a perspective where I now work primarily in JS/TS, prefer FP…

Oh yeah, for sure there could be performance implications to lazy sequences, but ideally the compiler or interpreter could figure out some nice optimizations.

Re: Lodash's Chain vs. Native Methods

#12

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})

It's a slightly contrived example, but it demonstrates a real situation that can come up. Suppose instead it was:

  const drinkingAge = 21

  persons
    .map(p => ({ ...p, canDrink: p.age >= drinkingAge }))
    .filter(p => p.canDrink)
    .slice(0, 5);
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:

  const drinkingAge = 21

  persons
    .filter(p => p.age >= drinkingAge)
    .map(p => ({ ...p, canDrink: p.age >= drinkingAge }))
    .slice(0, 5);
You could of course share the business logic by abstracting it into a function:

  const drinkingAge = 21

  function canDrink(person) {
    return person.age > drinkingAge
  }

  persons
    .filter(p => canDrink(p))
    .map(p => ({ ...p, canDrink: canDrink(p) }))
    .slice(0, 5);
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.

Re: Lodash's Chain vs. Native Methods

#13

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})

You’re overthinking it. And your version does 30 filters instead of 7 anyway.

Re: Lodash's Chain vs. Native Methods

#14

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})

Yes, but this will still filter through all the items, instead of stopping at 5.

Re: Lodash's Chain vs. Native Methods

#16

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

    const ps = persons
      .map(p => ({ ...p, age: p.age + 1 }))
      .filter(p => p.age >= 30)
      .slice(0, 5);
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.

Re: Lodash's Chain vs. Native Methods

#18
Comparisons of lodash to native methods are interesting but they always miss the real selling points of lodash in my opinion, which are the richer data structure manipulations like groupBy, partition, intersection, difference, union, etc.

Re: Lodash's Chain vs. Native Methods

#19

Comparisons of lodash to native methods are interesting but they always miss the real selling points of lodash in my opinion, which are the richer data structure manipulations like groupBy, partition, intersection, difference, union, etc.

This is very true, and what I mostly use lodash for these days. I also like the string utilities such as capitalize, and the utilities for dealing / getting random values.

Re: Lodash's Chain vs. Native Methods

#20

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})

It's a slightly contrived example, but it demonstrates a real situation that can come up. Suppose instead it was: const drinkingAge = 21 persons .map(p => ({ ...p, canDrink: p.age >= drinkingAge })) .filter(p => p.canDrink) .slice(0, 5); 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 f…

if you are using slice(0,5) then all the extra objects you mapped are lost and you have no reference to them.
Post reply on HN