Use a little bit of FP

A few examples of introducing functional programming into existing code.

Here are a couple of examples where I brought tiny functional programming bits into existing code bases. One is an internal API and another is a tiny snapshot testing utility.

Maybe for server code

At Cypress.io we have an API. This is your typical API with a controller object routing requests to data model layer. For example there is an endpoint that returns user information.

1
2
3
4
5
6
7
8
9
10
// controller
const User = require('./models/user')
function getAddress (req, res) {
const id = req.params.id
// check authenticated and authorized
User.get(id)
.then(info => {
res.json(info)
})
}

The model class User has a method get(id) which constructs user information from several sources. To construct full user information from promise-returning functions, we use Bluebird.props function; it is super helpful. Before you say anything: I know, I should be using GraphQL and async functions for this, but I am not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// model class
const Bluebird = require('bluebird')
User.get = function (id) {
Bluebird.props({
basic: getBasicUserInfo(),
address: getAddress(),
history: getHistory()
})
}
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(result => {
return result.customer.address
})
}

Everything is working, but we have decided to make everything the API returns be camel cased for consistency. So we convert the result of getAddress using object-to-camel-case.

1
2
3
4
5
6
7
8
9
const objectToCamelCase = require('object-to-camel-case')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(result => {
return result.customer.address
})
.then(objectToCamelCase)
}

Here is a problem. Sometimes a customer has no address in our system. Or maybe we are missing a customer record for this user completely! So we need to protect result.customer.address getter.

No problem, Ramda.path to the rescue!

1
2
3
4
5
6
7
8
const objectToCamelCase = require('object-to-camel-case')
const R = require('ramda')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.path(['customer', 'address']))
.then(objectToCamelCase)
}

But if the address or customer really are missing, we will pass undefined into objectToCamelCase() and it crashes. So we need an if statement.

1
2
3
4
5
6
7
8
const objectToCamelCase = require('object-to-camel-case')
const R = require('ramda')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.path(['customer', 'address']))
.then(address => address ? objectToCamelCase(address) : address)
}

That is one nasty if statement, that can only be marginally improved by picking a shorter non-descriptive variable name instead of address. Plus there might be other places where we need to guard against a non-existent address.

We might return an empty object, that is simple with Ramda.pathOr

1
2
3
4
5
6
7
8
const objectToCamelCase = require('object-to-camel-case')
const R = require('ramda')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.pathOr({}, ['customer', 'address']))
.then(objectToCamelCase)
}

This just pushes the problem to the edge of the computation (really to the client making the server request), making a check for existing address more complex.

What we really need is an easy and unambiguous way to signal that the result of a database query maybe empty. And if it is empty, we should skip any further computations over the data. Here is where Maybe comes in handy. There are several libraries that implement it, but I will pick folktale since it seems complete and popular.

Adding Maybe

Let us make our model return address wrapped in a Maybe instance. Install folktale using npm i -S folktale and use Maybe.fromNullable to wrap the address.

1
2
3
4
5
6
7
8
9
10
11
12
const objectToCamelCase = require('object-to-camel-case')
const R = require('ramda')
const Maybe = require('folktale/maybe')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.path(['customer', 'address']))
.then(Maybe.fromNullable)
// what should we do now?
// next callback function will receive a Maybe(address) object
.then(objectToCamelCase)
}

Instead of calling objectToCamelCase directly on the wrapped data, we call (x) => x.map(objectToCamelCase).

1
2
3
4
5
6
7
8
9
10
const objectToCamelCase = require('object-to-camel-case')
const R = require('ramda')
const Maybe = require('folktale/maybe')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.path(['customer', 'address']))
.then(Maybe.fromNullable)
.then(maybe => maybe.map(objectToCamelCase))
}

Because Ramda.map can call .map(cb) on anything with a map method, we can shorten the last line:

1
2
3
4
5
6
7
8
9
10
const objectToCamelCase = require('object-to-camel-case')
const R = require('ramda')
const Maybe = require('folktale/maybe')
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.path(['customer', 'address']))
.then(Maybe.fromNullable)
.then(R.map(objectToCamelCase))
}

So our model function resolves a promise with a Maybe instance. How does our controller change then? It needs to convert to plain values before serializing the result and sending it back to the client. Controller can also decide what to do for the cases when Maybe is of type Nothing - when there is no address. A good function to use in this case is Ramda.evolve:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// controller
const User = require('./models/user')
const toPlainValues = R.evolve({
// we could use default value here, but null is ok too
address: maybe => maybe.getOrElse()
})
function getAddress (req, res) {
const id = req.params.id
// check authenticated and authorized
User.get(id)
.then(toPlainValues)
// shortcut to res.json
.then(res.json.bind(res))
}

Function toPlainValues will extract actual value from address property (or return default value undefined from getOrElse()), leaving other properties unchanged. Then the plain object is sent as JSON to the client.

What about tests? Well if we mock data returned from the model instance in our tests, we probably already have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// controller-test.js
const R = require('ramda')
it('returns address', () => {
const info = {
basic: {
first: 'Joe',
last: 'Smith'
},
address: {
city: 'Boston'
}
}
sinon.stub(User, 'get').resolves(info)
superTest(app)
.get('/user')
.expect(200)
.then(R.prop('body'))
.then(user => {
// check user.address
})
})

The test can easily return Maybe address from the stub

1
2
3
4
5
6
7
8
9
10
const Maybe = require('folktale/maybe')
const info = {
basic: {
first: 'Joe',
last: 'Smith'
},
address: Maybe.of({
city: 'Boston'
})
}

Testing data wrapped in a Maybe is as simple as it was before.

Chaining to prevent nesting

What if we want to extract a zip code from the address? Let me just show this separately. We can wrap "plain" getAddress to return a Maybe by composing Maybe.fromNullable with getAddress.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const R = require('ramda')
const Maybe = require('folktale/maybe')

const getAddress = R.path(['customer', 'address'])
const getMaybeAddress = R.compose(Maybe.fromNullable, getAddress)
console.log(getMaybeAddress({
customer: {
address: {
city: 'Boston',
zip: 22222
}
}
}))
//> folktale:Maybe.Just({ value: {city: 'Boston', zip: 22222} })

Let us grab the zip code - we can use Maybe.map for this

1
2
3
4
5
6
7
8
9
10
11
const getZipCode = R.prop('zip')

console.log(getMaybeAddress({
customer: {
address: {
city: 'Boston',
zip: 22222
}
}
}).map(getZipCode))
//> folktale:Maybe.Just({ value: 22222 })

But the zip code might be missing. In that case we will get a Maybe with an undefined value.

1
2
3
4
5
6
7
8
console.log(getMaybeAddress({
customer: {
address: {
city: 'Unknown'
}
}
}).map(getZipCode))
//> folktale:Maybe.Just({ value: undefined })

This might be ok, but we probably want to be safe against an undefined zip code. What if we return a Maybe (zipcode)? We get a problem - we would get a nested Maybe inside a Maybe 😬

1
2
3
4
5
6
7
8
9
const getMaybeZipCode = R.compose(Maybe.fromNullable, getZipCode)
console.log(getMaybeAddress({
customer: {
address: {
city: 'Unknown'
}
}
}).map(getMaybeZipCode))
//> folktale:Maybe.Just({ value: folktale:Maybe.Nothing({ }) })

The trick to prevent nesting wrappers of the same time is to call .chain(cb) instead of .map(cb) if the function cb returns result already wrapped!

1
2
3
4
5
6
7
8
console.log(getMaybeAddress({
customer: {
address: {
city: 'Unknown'
}
}
}).chain(getMaybeZipCode))
//> folktale:Maybe.Nothing({ })

The above is what we want. Let us rewrite it for clarity to separate logic (functions) from data. Again, Ramda has us covered with Ramda.chain.

1
2
3
4
5
6
7
8
9
10
11
12
const getZip = R.pipe(
getMaybeAddress,
R.chain(getMaybeZipCode)
)
console.log(getZip({
customer: {
address: {
city: 'Unknown'
}
}
}))
//> folktale:Maybe.Nothing({ })

Easy peasy. Just remember: if you are returning a plain value, then use .map, and you are returning a Maybe use .chain.

Result (Either) for utility functions

When writing a snapshot testing utility snap-shot-core I needed a function to compare two values. In a simplest case, we could write something like this

1
2
3
4
5
6
7
8
9
10
11
12
13
function compare ({expected, value}) {
const e = JSON.stringify(expected)
const v = JSON.stringify(value)
if (e === v) {
return {
valid: true
}
}
return {
valid: false,
message: `${e} !== ${v}`
}
}

If the values are different we really want a more useful message with the difference. I have made snap-shot-compare that uses 3rd party libs for object and text comparison that generate excellent diffs. But the library still adheres to the convention. It returns an object with at least one property {valid: true|false}. If valid === false then there should be a message property with the difference text. Again, just like in the previous case with Maybe we have a "magic" value we are returning. Only now we not only want to control if - else, but pass the message data.

Folktale library has a wrapper for this case, and it is called Result. Other libraries might call it Either. Let us return a Result from the compare function.

1
2
3
4
5
6
7
8
9
const Result = require('folktale/result')
function compare ({expected, value}) {
const e = JSON.stringify(expected)
const v = JSON.stringify(value)
if (e === v) {
return Result.Ok()
}
return Result.Error(`${e} !== ${v}`)
}

We can easily test this logic, here a simple test that confirms that a Result instance is returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
// spec.js
const Result = require('folktale/result')
const la = require('lazy-ass')
describe('compare', () => {
const {compare} = require('./utils')

it('returns Result', () => {
const expected = 'foo'
const value = 'foo'
const r = compare({expected, value})
la(Result.hasInstance(r))
})
})

But what about actual values? It is easy to capture the entire instance as a snapshot (talk about a chicken and an egg problem here!)

1
2
3
4
5
6
const snapshot = require('snap-shot-it')
it('has error (snapshot)', () => {
const expected = 'foo'
const value = 'bar'
snapshot(compare({expected, value}))
})
1
$ DRY=1 npm test
saving snapshot "compare has error (snapshot) 1" for file src/utils-spec.js
{ '@@type': 'folktale:Result',
  '@@tag': 'Error',
  '@@value': { value: '"foo" !== "bar"' } }

While this certainly works for a test, I prefer not to store the internal implementation details in the snapshot. Instead we will get to the error value by passing snapshot as a callback to .orElse()

1
2
3
4
5
it('snapshots error value', () => {
const expected = 'foo'
const value = 'bar'
compare({expected, value}).orElse(snapshot)
})
1
$ DRY=1 npm test
saving snapshot "compare snapshots error value 1" for file src/utils-spec.js
"foo" !== "bar"

That is perfect, except for a tiny detail. If for some reason compare() returns Result.Ok our snapshot callback will not be triggered at all. Thus we need to guard against a successful Result - it is a mistake, and the test shoud fail. Make a utility function and use it as callback to the "happy" path.

1
2
3
4
5
6
7
8
9
10
11
const raise = () => {
throw new Error('Should not reach this')
}
it('snapshots error value', () => {
const expected = 'foo'
const value = 'bar'
compare({expected, value})
// prevents Result.Ok() from skipping error snapshot
.map(raise)
.orElse(snapshot)
})

note despite being similar to promises, all callbacks to .map() and other methods in Maybe and Result are executed synchronously, just like [].map() returns a new array synchronously.

Using the returned Result, and raising an exception is simple. Again we can use the orElse callback.

1
2
3
4
compare({expected, value}).orElse(message => {
debug('Test "%s" snapshot difference', specName)
throw new Error(message)
})

If you want to see all changes that went into moving to Result in this small utility package, take a look at the pull request #12. Because the change really affected the API of the module, I committed it as major breaking change. The bumped version is now 2.0.0.

Final thoughts

We have added a little bit of functional programming to our server code, and to a utility package. Each change made the result of the computation a little less ambiguous, and removed some complexity. These are very small changes, but it is a beachhead - we must start slow. Only by showing benefits we can win hearts and minds of other developers.

Aside: using Maybe and Result is orthogonal to switching from JavaScript to TypeScript. I believe adding static type checking improves your code, but this benefit is different from replacing if - else statements with Maybe type.