I like using compose to collapse
code into a single function. Imagine we have several functions and some data,
and would like to print full user names. Our initial code has bunch of functions
and the final "algorithm" function printNames
1 | function getUser(id) { |
Notice that the functions have different arity: some functions take a single
argument (formFullName
has arity 1), while others take several arguments
(concat
and getProperty
has arity 2).
Our goal is to collapse the function printNames
into a single function using compose
utility.
In order to compose functions, each individual function (aside from the very last one)
needs to have arity 1. This is because the return value of each previous function is going
to be passed into it.
1 | // same as foo(bar(baz(...))) |
Let us take a look at the simple case - getProperty
helper. It was used inside formFullName
to return user.first
and user.last
values.
1 | function formFullName(user) { |
We know the first argument in both invocations and can apply partial application to create two new functions with arity 1.
1 | function formFullName(user) { |
While we are applying getProperty
, notice that getUser
can be rewritten the same way.
In this case, we know the object (the second argument), but not the property (the first argument).
Since JavaScript does not have built-in application from the right, we can use a utility library
1 | const R = require('ramda') |
Let us make the code inside formFullName
composable.
Our biggest problem right now is the concat
function - it takes 2 arguments. Luckily, we
can rewrite it using R.join
1 | function concat(a, b) { // arity 2 |
How do we get a list of strings to join? By applying first
and last
property accessors
to the same user
object (using R.ap method). While we are at it,
let us use Ramda's built-in property access method R.prop.
1 | // original code |
Notice the great thing - since each function inside formFullName
has been
partially applied (using the built-in currying), each variable first, last, spacer, ...
points at a function with arity 1. The last line is a composable expression
spacer(apFirstLast(R.of(user)))
and can be rewritten
1 | return R.compose(spacer, apFirstLast, R.of)(user) |
Even better, there is a method to extract list of values from an object R.props that can replace these three lines
1 | const first = R.prop('first') // arity 1 |
1 | function formFullName(user) { |
If we can easily compose the result and our entire function is just a few lines of code, why not construct it as an expression?
1 | const formFullName = R.compose(R.join(' '), R.props(['first', 'last'])) |
If you are used to curried functions, the you are probably comfortable with the above shorthand notation. Otherwise, keeping the explicit function might be safer.
Let us transform another binary function print
into an unary function. Right now it
takes two arguments
1 | function print(id, s) { |
Instead of two separate arguments, let us take in a single array of values.
1 | function print(values) { |
Or simply
1 | const print = R.apply(console.log) |
Now let us print two values using an array.
1 | function printNames(ids) { |
There is a composition there!
1 | function printNames(ids) { |
We need to generate a list of values [id, formFullName(getUser(id))]
from a single id
.
This is simple if we apply a list of functions to the same value using
R.ap (the first value is produced using identity function).
We just need to transform the id
into list again [id]
to match the signature of R.ap
.
1 | function printNames(ids) { |
Again we have a candidate for composition - look at R.ap([R.identity, userName])(R.of(id))
.
Every time you see f(g(x))
you have found a R.compose
candidate.
1 | function printNames(ids) { |
And again we have at the last two lines of the function a candidate for the compose
1 | function printNames(ids) { |
which is the same as
1 | function printNames(ids) { |
Let us now plug in userValues
and userName
from each line into the next line
1 | function printNames(ids) { |
then (using white space for clarity)
1 | function printNames(ids) { |
It is a good idea to factor out the inner forEach
callback for clarity. Since it is just a single
compose now, we can use an expression instead of a function in this case
1 | const print = R.apply(console.log) |
We do not need an explicit print, since we are using it only inside printName
.
1 | const printName = R.compose( |
In the last function printNames
we are just iterating over a list - we can use
R.forEach for this.
1 | function printNames(ids) { |
R.forEach
is curried, thus it can be made into unary function right away
1 | function printNames(ids) { |
Now we can get rid of the explicit function altogether
1 | const printNames = R.forEach(printName) |
Here is our final code - much shorter than the original, and hopefully more robust (because Ramda itself has been tested thoroughly)
1 | const R = require('ramda') |
We have applied the following principles in this refactoring
- transform functions into unary ones (accepting just a single argument)
- move arguments around until the free argument is the very last one
- replace every code of the form
f(g(x))
withR.compose(f, g)(x)
expression
Once we are comfortable composing individual functions we can start using promise compositions (R.composeP) and even Monad-returning functions (R.composeK).