Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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>>>"
@danrspencer

This comment has been minimized.

Copy link

danrspencer commented Jun 28, 2017

I like the version of this at the end of this Medium post so you don't have to use an array.

I think it's also useful to provide a function similar to Ramda's pipe as sometimes it's more readable to go the other way, e.g.

const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const pipe = (...fns) => compose.apply(compose, fns.reverse());

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

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

console.log("Compose");
console.log(example('hello'));

console.log("Pipe");
console.log(example2('hello'));
@JamieMason

This comment has been minimized.

Copy link
Owner Author

JamieMason commented Jun 28, 2017

Like it @danrspencer 👍

@JamieMason

This comment has been minimized.

Copy link
Owner Author

JamieMason commented Aug 3, 2017

Updated to spread the arguments instead of taking an array

@nem035

This comment has been minimized.

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

This comment has been minimized.

Copy link

kevinwucodes commented Sep 27, 2017

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

@JamieMason

This comment has been minimized.

Copy link
Owner Author

JamieMason commented Oct 3, 2017

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.

@RomaSto

This comment has been minimized.

Copy link

RomaSto commented Feb 9, 2018

so much pain

@droganov

This comment has been minimized.

Copy link

droganov commented Feb 17, 2018

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

@JamieMason

This comment has been minimized.

Copy link
Owner Author

JamieMason commented Feb 19, 2018

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link

mtarnovan commented Sep 26, 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.

@Beraliv

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

huashiyiqike commented Jan 20, 2019

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

This comment has been minimized.

Copy link

noel-yap commented Feb 20, 2019

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
You can’t perform that action at this time.