Quick functional refactoring

Quickly stringing data transformation using standrd Ramda functions.

I was writing a small program to drive Cypress test runner via its module api to run the same spec file against multiple apps. There is a list of application names and we go through each app one by one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require('console.table')
const Promise = require('bluebird')
Promise.mapSeries(apps, testApp)
.then(results => {
// results is array of objects
// example:
// [{
// tests: 21,
// passes: 21,
// pending: 0,
// failures: 0,
// duration: '35 seconds',
// screenshots: 0,
// video: false,
// version: '1.4.1'
// }, ...]
console.table('App results', results)
})

The testApp function calls Cypress module to actually run the tests.

1
2
3
4
5
6
7
8
9
10
const cypress = require('cypress')
const testApp = app => {
return cypress
.run({
browser: args.browser,
env: {
app
}
})
}

Ok, so let's run it with two different apps, what do we see?

1
2
3
4
5
6
TodoMVC results
-------------------------------------------------------------------------
tests passes pending failures duration screenshots video version
----- ------ ------- -------- ---------- ----------- ----- -------
21 21 0 0 33 seconds 0 false 1.4.1
29 29 0 0 37 seconds 0 false 1.4.1

Hmm, not really useful. There are columns that are going to be the same: screenshots, video and version. And there is the most important column missing - the name of the app for each row! I also want to highlight the number of failures if there are any. Any value above zero in the failures column should be displayed in red, while zero should be displayed in green.

Ok, we are good programmers so we separate changing the shape of the object from changing colors. We can attach transforms to the promise returned from cypress.run

1
2
3
4
5
6
7
8
9
10
11
const testApp = app => {
return cypress
.run({
browser: args.browser,
env: {
app
}
})
.then(addInfo)
.then(addColors)
}

The function is addInfo that will remove some properties and add others. We can write it in my usual "procedural" style. That's how I usually start my code - just get the thing working.

1
2
3
4
5
6
7
8
9
10
11
12
const testApp = app => {
const addInfo = results => {
delete results.screenshots
delete results.video
delete results.version
results.app = app
return results
}
return cypress
.run(...)
.then(addInfo)
}

Hmm, is this function testResults doing what I think it should be doing? How can I test it - it is deep inside the testApp closure, hard to reach to test it. The only reason it has to be there is because it needs variable app. Let us solve this problem - let us move addInfo outside. We can pass the app name as an argument.

1
2
3
4
5
6
7
const addInfo = (results, app) => {
delete results.screenshots
delete results.video
delete results.version
results.app = app
return results
}

Hmm, but using this function is a little inconvenient.

1
2
3
return cypress
.run(...)
.then(results => addInfo(results, app))

See what I mean? We know app right away, but have to wait until the cypress.run promise resolves to get the results value. In situations like this, when we know one argument much earlier than the other arguments, we should place first in the functions signature. So this order would be preferred. Now we can do partial application right in .then callback.

1
2
3
const addInfo = (app, results) => { ... }
cypress.run(...)
.then(addInfo.bind(null, app))

Great, JavaScript can even do partial application in ES5, but we can shorten our code using a helper library like Ramda.

1
2
3
const R = require('ramda')
cypress.run(...)
.then(R.partial(addInfo, app))

But we can make this even more convenient by currying the addInfo function - in ES6 it is so simple to do, no extras required.

1
2
3
4
const addInfo = app => results => {...}
// look how it is easy now
cypress.run(...)
.then(addInfo(app))

Easy peasy.

Let us look inside the function addInfo itself. The point of functional refactoring is to split unrelated operations into many small single purpose functions and then compose back slides. We currently have two different things going on - deleting some properties and adding a new one.

1
2
3
4
5
6
// deleting
delete results.screenshots
delete results.video
delete results.version
// adding
results.app = app

Deleting a list of properties from an object is so common, there is a function in Ramda that does exactly that R.omit. And it is already designed "the right way" - the names of properties we know is at first position, and the function is curried. We can even move it out of addInfo into its own .then(...) step.

1
2
3
cypress.run(...)
.then(R.omit(['screenshots', 'video', 'version']))
.then(addInfo(app))

Now let's add the app property. There is no built-in method for this - and you will see why shortly (there is more general and powerful one). We could make our own tiny function just for this. That is what I do a lot - writing tiny functions when I do not want to bring heavy guns.

1
2
3
4
5
6
7
const setProp = name => value => object => {
object[name] = value
return object
}
cypress.run(...)
.then(R.omit(['screenshots', 'video', 'version']))
.then(setProp('app', app))

Great, we have triple function setProp! But it is not as polished as R.omit. We can notice this by inspecting the input and output objects.

1
2
const result = R.omit(['foo'])(input)
result === input // false

R.omit does not mutate the input object - it creates a new one without the listed properties. Our function setProp mutates the object - making it harder to rely on it. Any object passed into setProp will be forever changed. Or not, you really cannot tell in JavaScript. So lets look at what Ramda can give us. Working with object properties is done via lenses. First we create a lens to look at specific property app. Then we use a different function to set a specific value via this lens on an object that will arrive later.

1
2
3
4
const lens = R.lensProp('app')
cypress.run(...)
.then(R.omit(['screenshots', 'video', 'version']))
.then(R.set(lens, app))

We don't need to make lens object separately, we can just make it in place when needed.

1
2
3
cypress.run(...)
.then(R.omit(['screenshots', 'video', 'version']))
.then(R.set(R.lensProp('app'), app))

Perfect, remember why we have started this? So that our function appInfo is testable. Look what has happened now - we have eliminated appInfo completely. Instead we have two very simple functions from a well tested and widely used library. It is very unlikely that anything will go wrong in R.omit or R.set functions, so we don't have to worry.

Next we should look at the addColors function. My original implementation is using chalk.

1
2
3
4
5
6
const addColors = results => {
results.failures = results.failures
? chalk.red(results.failures)
: chalk.green(results.failures)
return results
}

Notice the similarity to our previous refactoring - we are working with specific property on an object that is yet to arrive. So we will make a lens to "look" at the property failures and we are going to modify its value and will put it back into the object. Or rather we will make a new object with updated property (thanks immutability).

1
2
3
const failuresLens = R.lensProp('failures')
const colorFailures = n => (n ? chalk.red(n) : chalk.green(n))
const addColors = results => R.over(failuresLens, colorFailures, results)

Notice how we split the function into its atomic operations? And the great thing - the last line is NOT needed - all it does is passing results as the last argument to R.over. Since every function in Ramda is curried, we can get rid of it. Plus we can create the lens in place, bringing our logic to super simple two-liner.

1
2
const colorFailures = n => (n ? chalk.red(n) : chalk.green(n))
const addColors = R.over(R.lensProp('failures'), colorFailures)

So here is our final code - pure functions, passing and modifying data along the chain.

1
2
3
4
5
6
7
const colorFailures = n => (n ? chalk.red(n) : chalk.green(n))
const addColors = R.over(R.lensProp('failures'), colorFailures)
return cypress
.run(...)
.then(R.omit(['screenshots', 'video', 'version']))
.then(R.set(R.lensProp('app'), app))
.then(addColors)

And here the result - nice little table we got here.

Results

PS: you do not have to code every object transform by hand. I wrote a tiny utility change-by-example that given two objects computes the transformation using Ramda functions and especially lenses