Refactoring to compose

What to do if you want to compose multiple functions (hint - make them unary).

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function getUser(id) {
return _users[id]
}
function getProperty(key, object) {
return object[key]
}
function formFullName(user) {
return concat(
getProperty('first', user),
getProperty('last', user)
)
}
function concat(a, b) {
return a + ' ' + b
}
function print(id, s) {
console.log(id, s)
}
function printNames(ids) {
ids.forEach(function (id) {
const user = getUser(id)
const fullName = formFullName(user)
print(id, fullName)
})
}
const _users = {
'100': {first: 'joe', last: 'dow'},
'101': {first: 'mary', last: 'smith'}
}
const ids = ['100', '101']
printNames(ids)
/*
100 joe dow
101 mary smith
*/

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
2
3
4
5
6
// same as foo(bar(baz(...)))
compose(
foo, // final result
bar, // result goes into "foo"
baz // result goes into "bar"
)(...)

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
2
3
4
5
6
function formFullName(user) {
return concat(
getProperty('first', user),
getProperty('last', user)
)
}

We know the first argument in both invocations and can apply partial application to create two new functions with arity 1.

1
2
3
4
5
6
7
8
function formFullName(user) {
const first = getProperty.bind(null, 'first')
const last = getProperty.bind(null, 'last')
return concat(
first(user),
last(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
2
3
4
5
6
7
8
9
10
const R = require('ramda')
function getProperty(key, object) {
return object[key]
}
const _users = {
'100': {first: 'joe', last: 'dow'},
'101': {first: 'mary', last: 'smith'}
}
const getUser = R.partialRight(getProperty, [_users])
// getUser('100') returns user with id '100'

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
2
3
4
5
6
function concat(a, b) { // arity 2
return a + ' ' + b
}
// same as
const spacer = R.join(' ') // arity 1
spacer([a, b])

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// original code
function formFullName(user) {
const first = getProperty.bind(null, 'first')
const last = getProperty.bind(null, 'last')
return concat(
first(user),
last(user)
)
}
// transformed into steps
function formFullName(user) {
const first = R.prop('first') // arity 1
const last = R.prop('last') // arity 1
const apFirstLast = R.ap([first, last]) // arity 1
const spacer = R.join(' ') // arity 1
// R.of has arity 1: a -> [a]
return spacer(apFirstLast(R.of(user))) // composable!
}

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
2
3
const first = R.prop('first') // arity 1
const last = R.prop('last') // arity 1
const apFirstLast = R.ap([first, last]) // arity 1
1
2
3
4
5
function formFullName(user) {
const firstLast = R.props(['first', 'last']) // arity 1
const spacer = R.join(' ') // arity 1
return R.compose(spacer, firstLast)(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
2
3
function print(id, s) {
console.log(id, s)
}

Instead of two separate arguments, let us take in a single array of values.

1
2
3
4
function print(values) {
// values is an array
console.log.apply(console, values)
}

Or simply

1
2
const print = R.apply(console.log)
// call print with an Array: print(['foo', 'bar'])

Now let us print two values using an array.

1
2
3
4
5
function printNames(ids) {
ids.forEach(function (id) {
print([id, formFullName(getUser(id))])
})
}

There is a composition there!

1
2
3
4
5
6
function printNames(ids) {
const userName = R.compose(formFullName, getUser)
ids.forEach(function (id) {
print([id, userName(id))])
})
}

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
2
3
4
5
6
7
function printNames(ids) {
ids.forEach(function (id) {
const userName = R.compose(formFullName, getUser)
const values = R.ap([R.identity, userName])(R.of(id))
print(values)
})
}

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
2
3
4
5
6
7
8
function printNames(ids) {
ids.forEach(function (id) {
const userName = R.compose(formFullName, getUser)
const userValues = R.compose(R.ap([R.identity, userName]), R.of)
const values = userValues(id)
print(values)
})
}

And again we have at the last two lines of the function a candidate for the compose

1
2
3
4
5
6
7
function printNames(ids) {
ids.forEach(function (id) {
const userName = R.compose(formFullName, getUser)
const userValues = R.compose(R.ap([R.identity, userName]), R.of)
print(userValues(id))
})
}

which is the same as

1
2
3
4
5
6
7
function printNames(ids) {
ids.forEach(function (id) {
const userName = R.compose(formFullName, getUser)
const userValues = R.compose(R.ap([R.identity, userName]), R.of)
R.compose(print, userValues)(id)
})
}

Let us now plug in userValues and userName from each line into the next line

1
2
3
4
5
6
function printNames(ids) {
ids.forEach(function (id) {
const userName = R.compose(formFullName, getUser)
R.compose(print, R.compose(R.ap([R.identity, userName]), R.of))(id)
})
}

then (using white space for clarity)

1
2
3
4
5
6
7
8
9
10
11
function printNames(ids) {
ids.forEach(function (id) {
R.compose(
print,
R.compose(
R.ap([R.identity, R.compose(formFullName, getUser)]),
R.of
)
)(id)
})
}

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
2
3
4
5
6
7
8
9
10
11
const print = R.apply(console.log)
const printName = R.compose(
print,
R.compose(
R.ap([R.identity, R.compose(formFullName, getUser)]),
R.of
)
)
function printNames(ids) {
ids.forEach(printName)
}

We do not need an explicit print, since we are using it only inside printName.

1
2
3
4
5
6
7
8
9
10
const printName = R.compose(
R.apply(console.log),
R.compose(
R.ap([R.identity, R.compose(formFullName, getUser)]),
R.of
)
)
function printNames(ids) {
ids.forEach(printName)
}

In the last function printNames we are just iterating over a list - we can use R.forEach for this.

1
2
3
function printNames(ids) {
R.forEach(printName, ids)
}

R.forEach is curried, thus it can be made into unary function right away

1
2
3
function printNames(ids) {
R.forEach(printName)(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const R = require('ramda')
const _users = {
'100': {first: 'joe', last: 'dow'},
'101': {first: 'mary', last: 'smith'}
}
const getUser = R.partialRight(R.prop, [_users])
const formFullName = R.compose(R.join(' '), R.props(['first', 'last']))
const printName = R.compose(
R.apply(console.log),
R.compose(
R.ap([R.identity, R.compose(formFullName, getUser)]),
R.of
)
)
const printNames = R.forEach(printName)
const ids = ['100', '101']
printNames(ids)
/*
100 joe dow
101 mary smith
*/

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)) with R.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).