DEV Community

Munawwar
Munawwar

Posted on • Updated on

Lodash chaining alternative

Those of you dealing with data transforms/manipulations for charting/dashboards/whatever, need no introduction to Lodash library, and have been using it happily on the backend, frontend etc

The problem

However there is one method Lodash has that's so useful but has its performance implications on the frontend.. namely chain().
On the frontend if you naively import chain the entire lodash library will end up in your bundle.. and the entire lodash library isn't small. Backend code doesn't care about some extra bloat.

That's sad. chain is very useful and I would like to use chaining on the frontend without having to take a performance hit. So.. what is the solution?

What does Google say?

Googling around you would see many suggestions to use lodash/fp's flow() method. You can see the code from this 2016 post

import map from "lodash/fp/map";
import flatten from "lodash/fp/flatten";
import sortBy from "lodash/fp/sortBy";
import flow from "lodash/fp/flow";
flow(
  map(x => [x, x*2]),
  flatten,
  sortBy(x => x) 
)([1,2,3]);
Enter fullscreen mode Exit fullscreen mode

It works.. it keeps the bundle size small and gives you chaining capability.

But there is something bothersome about that code...

_.chain begins with the data you need to manipulate and then you call the transforms.. whereas flow() begins with the transforms and ends with the data you want to manipulate. This isn't natural to read. It needs to be flipped around.

[From flow()'s perspective it is built as intended. flow is potentially built for reuse. Fine. However we still miss a closer alternative to chain.]

Alternative solution

My ideal syntax would be the following (for the same code sample from above):

chain([1,2,3])
  (map, x => [x, x*2])
  (flatten)
  (sortBy, x => x)
  ();
Enter fullscreen mode Exit fullscreen mode

However most linter configurations would complain about the indented parentheses. So we need a dummy function and a .value() to break out of the chain (like lodash already does)

chain([1,2,3])
  .fn(map, x => [x, x*2])
  .fn(flatten)
  .fn(sortBy, x => x)
  .value();
Enter fullscreen mode Exit fullscreen mode

Overall if you squint your eyes and ignore the .fn()s, then it looks very similar to lodash's _.chain syntax. And there is a way to implement this. I'll dive straight into the implementation which is small and probably don't need too much explanation:

function chain(value) {
  return {
    fn: (func, ...args) => chain(func(value, ...args)),
    value: () => value,
  };
}
Enter fullscreen mode Exit fullscreen mode

This implementation brings some new opportunity considering how generic the approach is.

The function doesn't know anything about lodash. It takes in any function. So you can write custom functions or use Math.* or Object.* functions

chain({prop: 2, fallback: 1})
  .fn((obj) => obj.prop || obj.fallback)
  .fn(Math.pow, 2)
  .value(); // result = 4
Enter fullscreen mode Exit fullscreen mode

Improvement

With a slight modification, we can make it call any function on result objects.

Which mean for arrays, we can use native array map, filter etc, and we don't really need to use lodash's functions there. We should be able to do something like the following (using same example from before):

chain([1,2,3])
  .fn('map', x => [x, x*2])
  // ... blah
  .value();
Enter fullscreen mode Exit fullscreen mode

Instead of passing the function here we put a name of the method to be invoked from the intermediate result object/array. The implementation of fn will change to the following:

    fn(func, ...args) {
      if (typeof func === 'function') {
        return chain(func(value, ...args));
      }
      return chain(value[func](...args));
    },
Enter fullscreen mode Exit fullscreen mode

We can further improve this to handle promises / async functions:

    fn(func, ...args) {
      if (value instanceof Promise) {
        return chain(value.then((result) => {
          if (typeof func === 'string') {
            return result[func](...args);
          }
          return func(result, ...args);
        }));
      }
      if (typeof func === 'string') {
        return chain(value[func](...args));
      }
      return chain(func(value, ...args));
    },
Enter fullscreen mode Exit fullscreen mode

And then it can be used as follows:

const dates = await chain(Promise.resolve([1, 2, 3]))
  // with method name
  .fn('map', x => x.toString())
  // with functions
  .fn((arr) => arr.map(x => new Date(x)))
  .value();
Enter fullscreen mode Exit fullscreen mode

I believe this is an improvement to the popular approaches suggested out there on the interwebz. Check it out, try it out.. criticism welcome.

That's all folks. Hope you liked my short, hastily written post.

Full code below:

/** @type {import('./chain').default} */
export default function chain(value) {
  return {
    fn(func, ...args) {
      if (value instanceof Promise) {
        return chain(value.then((result) => {
          if (typeof func === 'string') {
            return result[func](...args);
          }
          return func(result, ...args);
        }));
      }
      if (typeof func === 'string') {
        return chain(value[func](...args));
      }
      return chain(func(value, ...args));
    },
    value() {
      return value;
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

chain.d.ts

declare type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer P) => any ? P : never;
declare type MethodNames<T> = keyof {
  [K in keyof T]: T[K] extends ((...arg: any[]) => any) ? K : never;
};

export default function chain<V>(val: V): {
  fn<Func extends (v: Awaited<V>, ...args: any[]) => any>(func: Func, ...args: ParametersExceptFirst<Func>): ReturnType<typeof chain<ReturnType<Func>>>;
  fn<Name extends MethodNames<Awaited<V>>>(func: Name, ...args: Awaited<V> extends {
    [k: string | number | symbol]: any;
  } ? Parameters<Awaited<V>[Name]> : never): ReturnType<typeof chain<ReturnType<Awaited<V>[Name]>>>;
  value(): V;
};
export {};
Enter fullscreen mode Exit fullscreen mode

EDIT 2022: Added type definition and improved implementation
EDIT 2024: Added promise / async function handling

Top comments (6)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

I think the point of flow is that it defines a proper functional pipeline as a callable, that you will call later with the data. In functional programming, the data is normally last and the flow itself is also chainable and can be included inside a wider functional pipe. Chaining is the other way around. I'd argue that flow in the order it is presented by lodash is perfectly correct and makes total logical sense if you use it to define the function:

const processInputData = flow(
  map(x => [x, x*2]),
  flatten,
  sortBy(x => x) 
)

processInputData([1,2,3])
processInputData([4,5,6])

const complexProcess = flow(
   processInputData,
   map(([a,b])=>[a,b,a*b,a+b])
)

complexProcess([7,8,9])
Enter fullscreen mode Exit fullscreen mode
Collapse
 
munawwar profile image
Munawwar

Got it. Yet it still leaves for a closer alternative for chain for the front end.

Collapse
 
seanmclem profile image
Seanmclem

but the function still uses chain()

Thread Thread
 
munawwar profile image
Munawwar

Where? That's my own chain implementation not using lodash chain.

Collapse
 
rsslldnphy profile image
Russell Dunphy

I like this a lot. Very neat solution!

I've just been reading about the history of the various attempts at getting a pipe operator into Javascript, and its current (albeit stalled) direction, and imo this looks a lot cleaner and easier to understand than the Hack-style pipe operator that seems to have the most chance of actually getting adopted.

Some of the suggestions for what the placeholder character should be (here) would lead to some pretty nasty code imo. one of the suggestions for the placeholder character is ^^, which would lead to :

3
  |> double(^^)
  |> plus(^^ + 4)
  |> ^^.toString()
Enter fullscreen mode Exit fullscreen mode

with your chain function, on the other hand, this becomes:

chain(3)
  .fn(double)
  .fn(plus, 4)
  .fn('toString')
  .value();
Enter fullscreen mode Exit fullscreen mode

a LOT cleaner and easier to understand and doesn't need special syntax to make it work.

i do think an F# style pipe operator would be the nicest but it doesn't sound like that'd ever get accepted into javascript:

3
  |> double
  |> x => plus(x, 4)
  |> x => x.toString()
Enter fullscreen mode Exit fullscreen mode

Also - while I think what Mike Talbot says in another comment about flow defining a functional pipeline by composing functions is true, the chain-style approach is just as valid (and is the approach taken by most idiomatic Clojure code, for example, where comp is much rarer than the threading macros).

Collapse
 
munawwar profile image
Munawwar • Edited

About flow, if you want to reuse, then flow makes sense. But again with chain you could do:

const operation1 = (x) => chain(x).fn(doSomething1).value();

const operation2 = (x) => chain(x).fn(doSomething2).value();
Enter fullscreen mode Exit fullscreen mode

And then reuse:

chain(y)
  .fn(operation2)
  .fn(operation1)
  .value()
Enter fullscreen mode Exit fullscreen mode