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 | // controller |
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 | // model class |
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 | const objectToCamelCase = require('object-to-camel-case') |
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 | const objectToCamelCase = require('object-to-camel-case') |
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 | const objectToCamelCase = require('object-to-camel-case') |
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 | const objectToCamelCase = require('object-to-camel-case') |
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 | const objectToCamelCase = require('object-to-camel-case') |
Instead of calling objectToCamelCase
directly on the wrapped data, we call
(x) => x.map(objectToCamelCase)
.
1 | const objectToCamelCase = require('object-to-camel-case') |
Because Ramda.map can call .map(cb)
on anything with a map
method, we can shorten the last line:
1 | const objectToCamelCase = require('object-to-camel-case') |
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 | // controller |
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 | // controller-test.js |
The test can easily return Maybe address
from the stub
1 | const Maybe = require('folktale/maybe') |
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 | const R = require('ramda') |
Let us grab the zip code - we can use Maybe.map
for this
1 | const getZipCode = R.prop('zip') |
But the zip code might be missing. In that case we will get a Maybe
with
an undefined
value.
1 | console.log(getMaybeAddress({ |
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 | const getMaybeZipCode = R.compose(Maybe.fromNullable, getZipCode) |
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 | console.log(getMaybeAddress({ |
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 | const getZip = R.pipe( |
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 | function compare ({expected, value}) { |
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 | const Result = require('folktale/result') |
We can easily test this logic, here a simple test that confirms that a
Result
instance is returned.
1 | // spec.js |
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 | const snapshot = require('snap-shot-it') |
1 | $ DRY=1 npm test |
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 | it('snapshots error value', () => { |
1 | $ DRY=1 npm test |
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 | const raise = () => { |
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 | compare({expected, value}).orElse(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.