Why compose() is right-to-left

Published on in JavaScript

Functions composed together with compose() are called from right to left. It feels unintuitive at first, but it's conventional and kind of makes sense.

Table of contents

What's compose()?

I was recently reading the book Professor Frisby's Mostly Adequate Guide to Functional Programming [in JavaScript].[1] (It's a good book, but became too much for me somewhere around chapter 8 or 9. )

Chapter 5, Coding by Composing, introduces a compose() function for composing functions together:

const compose = (...fns) => (...args) =>
  fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0]

(There's also Ramda's R.compose, and Lodash's _.flowRight which is aliased to _.compose in lodash/fp.)

Example usage from the book:

const toUpperCase = (x) => x.toUpperCase()
const exclaim = (x) => `${x}!`
const shout = compose(exclaim, toUpperCase)

shout('send in the clowns') // "SEND IN THE CLOWNS!"

shout is a function composed of the exclaim and toUpperCase functions: it first calls toUpperCase and then exclaim.

Calling shout is the same as calling the two functions "manually" (without using compose):

exclaim(toUpperCase('send in the clowns')) // "SEND IN THE CLOWNS!"

The order of the two functions doesn't matter in this case because the end result is the same with either order.

Another example (a bit contrived) from the book; here the order does matter:

const head = (x) => x[0]
const reverse = reduce((acc, x) => [x, ...acc], [])
const last = compose(head, reverse)

last(['jumpkick', 'roundhouse', 'uppercut']) // 'uppercut'

last is a function composed of the head and reverse functions: it first calls reverse and then head.

Again, calling last is the same as calling the two functions "manually":

head(reverse(['jumpkick', 'roundhouse', 'uppercut'])) // 'uppercut'

The order of the two functions matters in this case: calling head before reverse wouldn't work at all because reverse expects an array. (If reverse worked with strings as well, the result would be 'kcikpmuj', which is totally different from 'uppercut'.)

Why right-to-left is unintuitive

When you encounter compose(), you have to read its arguments backwards. JavaScript, like most human languages, is written from left to right, so having to read the arguments backwards is unintuitive.

When you encounter const myFn = compose(many, args, here), you have to jump to the last argument of compose to see what's the first function that myFn calls, and then backtrack to the left one argument at a time.

So, compose(A, B, C) does not mean "first call A, then B and then C" like one could expect. It's the opposite: "first call C, then B and then A." πŸ€Έβ€β™‚οΈ

Compare also with method chaining, which works from left to right:

'send in the clowns'.toUpperCase().exclaim()
// vs
compose(exclaim, toUpperCase)('send in the clowns')

['jumpkick', 'roundhouse', 'uppercut'].reverse().head()
// vs
compose(head, reverse)(['jumpkick', 'roundhouse', 'uppercut'])

Why right-to-left makes sense (kind of)

The book justifies the right-to-left order very shallowly:

We could define a left to right version, however, we mirror the mathematical version much more closely as it stands. That's right, composition is straight from the math books.

It's been too long since high school math, so I had to look up Function composition (mathematical concept) on Wikipedia. An example[2]:

(π‘“βˆ˜π‘”βˆ˜β„Ž)(𝑧) = 𝑓(𝑔(β„Ž(𝑧)))

Notice how the order of the functions 𝑓, 𝑔 and β„Ž is the same on both sides of the equals sign.

This is true in JavaScript as well: compose(A, B, C)(arg) is the same as A(B(C(arg))). Put more visually:

compose(A, B, C)(arg)
//      ↓  ↓  ↓  ↓↓↓
        A( B( C( arg ) ) )

If you think of compose() this way, it kind of makes sense.

pipe() is left-to-right and more intuitive

A "left-to-right compose()" is called pipe().

For example, Ramda has R.pipe, and Lodash has _.flow which is aliased to _.pipe in lodash/fp. There's also an ECMAScript proposal for adding a pipe/pipeline operator (|>) to JavaScript.

An example from Ramda's documentation:

const f = R.pipe(Math.pow, R.negate, R.inc)
f(3, 4) // -(3^4) + 1

Compare with R.compose:

const f = R.compose(R.inc, R.negate, Math.pow)
f(3, 4) // -(3^4) + 1

Piping is so much more intuitive that I wonder what are the arguments for favoring compose. "That's how it works in math" is one argument, but how good an argument is it? If you know other arguments, please tell me!

Footnotes

  1. The book is licensed under a CC BY-SA 4.0 license. Some code samples on this page are from the book; I have formatted them with Prettier. ↩

  2. I was wondering why I couldn't find a Unicode symbol for "mathematical italic small h" (β„Ž) even though I could find symbols for "mathematical italic small f" (𝑓) and "mathematical italic small g" (𝑔). Apparently because when the Unicode symbols for mathematical italic small letters were defined, β„Ž had already a different name for it: "Planck constant." Interesting! Source: Why are there holes in the Unicode table? on Stack Overflow. ↩