Skip to content

Instantly share code, notes, and snippets.

@JamieMason
Last active May 17, 2022 17:38
Show Gist options
  • Save JamieMason/172460a36a0eaef24233e6edb2706f83 to your computer and use it in GitHub Desktop.
Save JamieMason/172460a36a0eaef24233e6edb2706f83 to your computer and use it in GitHub Desktop.
ES6 JavaScript compose function

ES6 JavaScript Compose Function

Definition

const compose = (...fns) =>
  fns.reduceRight((prevFn, nextFn) =>
    (...args) => nextFn(prevFn(...args)),
    value => value
  );

Example

Create the function, composed of three others:

const example = compose(
  val => { console.log(1); return `1<${val}>`; },
  val => { console.log(2); return `2<${val}>`; },
  val => { console.log(3); return `3<${val}>`; }
);

Call the function:

example('hello')

Console output is:

3
2
1
"1<2<3<hello>>>"
@parkerault
Copy link

parkerault commented Dec 6, 2018

It took me about an hour of stepping through the debugger to figure out what the point of spreading ...args is (the resulting function has the same arity as the first function called in the chain, but all subsequent functions must have an arity of one). With a little tweak, you can use destructuring assignment to create a multivariate function chain, as long as all the composed functions have the same arity.

const composeMultivariate = (...fns) => fns.reduce((f, g) => (...xs) => f(...g(...xs)));

var foo = composeMultivariate(
  (i, j) => [i+1, j+1],
  (g, h) => [g+1, h+1],
  (e, f) => [e+1, f+1],
  (c, d) => [c+1, d+1],
  (a, b) => [a+1, b+1]
);

const [a, b] = foo(0, 1);
console.log(a, b); // 5, 6

Or if you want to generalize it so you can use it as a normal compose:

const compose = (...fns) => fns.reduce((f, g) => (...xs) => {
  const r = g(...xs);
  return Array.isArray(r) ? f(...r) : f(r);
});

Of course you can always just cram all your arguments into an object and skip all the bother. :)

It's fascinating how the reducer creates closures over the inner function and the composed function unrolls them in reverse. If you haven't run it through a step debugger you really should!

@huashiyiqike
Copy link

Looks twisted, I agree with @nem035
It can simply be
const compose = (...fns) => fns.reduce((a,b) => (...val) => a(b(...val)))

Why not just use reduce for composing and reduceRight for piping?

const compose2 = (f, g) => (...args) => f(g(...args))
const compose = (...fns) => fns.reduce(compose2);
const pipe = (...fns) => fns.reduceRight(compose2);

It would be more performant because we don't need to reverse the array and it's cleaner IMHO.

@noel-yap
Copy link

Similar to @parkerault, I have (in TypeScript):

export module Functions {
  export function compose(...fns: ((...any) => any)[]): any {
    return (...args: any[]) => {
      return fns.reduceRight((arg: any, f: (...any) => any) => {
        return (Array.isArray(arg)) ? f(...arg) : f(arg);
      }, args);
    };
  }
}

which passes the Ava test:

test('compose should compose', t => {
  const g = (x, y) => x + y;
  const f = n => n * 2;

  const h = Functions.compose(f, g);

  t.is(42, h(19, 2));
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment