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>>>"
@nem035
Copy link

nem035 commented Aug 9, 2017

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.

@kevinwucodes
Copy link

I'm confused about value => value. Isn't that the initialValue for the reducer? Why is this a function?

@JamieMason
Copy link
Author

Hello!
@kevinwucodes the idea there was so that prevFn is always a function.
@nem035 I like that, thanks. I wasn't aware reduceRight was available.

@droganov
Copy link

droganov commented Feb 17, 2018

can you compose(console.log)('title', 'body');?

@JamieMason
Copy link
Author

@droganov I wonder if you're thinking about partial application rather than function composition there? when called, what should the function in your example output?

@qm3ster
Copy link

qm3ster commented Jun 13, 2018

If only there was a way to type this function over an arbitrary length tuple in TS.

Also, would it make sense, for performance, to define static returns for a finite set of lengths?

To make compose(h,g,f) return (...args)=>h(g(f(...args))) instead of (...args)=>h(((...args)=>g(((...args)=>f(...args))(...args)))(...args))

@mtarnovan
Copy link

I think it's clearer like this:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x)

This avoids having to pass an identity function as the initial value of the reduce callback and also makes the return type more obvious.

@Beraliv
Copy link

Beraliv commented Oct 10, 2018

I think it's clearer like this:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x)

This avoids having to pass an identity function as the initial value of the reduce callback and also makes the return type more obvious.

How would you pass a set of arguments instead of x?
Probable solution is:

const compose = (...fns) => (...args) => fns.reduceRight((params, f) => Array.isArray(params) ? f(...params) : f(params), args)

And possible to have

const compose = (...fns) => fns.reduceRight((outer, inner) => (...args) => outer(inner(...args)))

without initialValue as it takes the last one as a start

@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