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
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. β©
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. β©