This is the fifth part of a series of posts where I show you how to use functional programming techniques to improve every day code. If you want to follow along I suggest starting from the beginning:
In the last couple of posts we discovered a pattern for structuring code that other people have discovered and called, monads. It turns out that this pattern is in many places you might not even realize. In Javascript we have Promises, which while not implemented as a monad in the strict sense, do behave a lot like monads do. We saw a practical example of using this pattern to structure UIs. In this post we’re going to look at patterns you might find in imperative code and how you can use monads to refactor that code to be pure and composable.
Almost every program we work with has to deal with input from somewhere. When this input comes from users our code should be defensive to prevent errors and unexpected behavior. In imperative programs this often means that a good amount of our code is spent checking and validating input at the edges of our program.
You might find code like this:
const moment = require('moment')
const covertUnixTimestamp = timeStamp =>
moment(new Date(parseInt(timeStamp)))
.format('YYYY-MM-DD HH:mm Z')
What will this return when timeStamp
is undefined
? It will return
the string, “Invalid date.” And if we give it a valid integer or
string-like integer we should get an timestamp formatted according to
that constant on the last line.
It turns out the pattern we have here is a function that can either
succeed or fail, do one thing or another, etc. It sounds so mundane
when we say it like that. Well it turns out that if we use the
Either
pattern we can control a lof the complexity of validating
input and we can compose our creations from smaller functions.
First let’s use a few helper libraries: ramda
and
fantasy-eithers
. We’ll start with a basic example:
const {Left: Fail, Right: Ok} = require('fantasy-eithers')
const ramda = require('ramda')
const fromNullable = v => R.isNil(v) ? Fail(v) : Ok(v)
What are Left
and Right
? You can think of them like constructors
for the Either
type. Here we rename them using ES6 destructuring
syntax into Fail
and Ok
to make our code a little more clear. The
original names were just a covention based on their position in the
type-signature of Either
.
Our function, fromNullable
, creates an Either
type. If the
paramter is null
or undefined
then we get Fail
and otherwise we
get Ok
. Pretty simple so far, right?
Let’s test it out:
> fromNullable(undefined)
definitions { l: undefined }
> fromNullable(null)
definitions { l: null }
> fromNullable('foo')
definitions { r: 'foo' }
The value being printed out here is an implementation detail of the
library we’re using. The l
means that the object being returned is
constructed from the Left
constructor. The cool thing is that we can
safely .map
over the result:
> fromNullable(undefined).map(x => x + '!')
definitions { l: undefined }
> fromNullable('foo').map(x => x + '!')
definitions { r: 'foo!' }
> fromNullable('world').map(x => x + '!').map(x => `Hello, ${x}`)
definitions { r: 'Hello, world!' }
> fromNullable(undefined).map(x => x + '!').map(x => `Hello, ${x}`)
definitions { l: undefined }
Well this is Javascript so it’s not exactly type safe:
> fromNullable({}).map(x => x + '!')
definitions { r: '[object Object]!' }
But it does give us some structure for our refactoring. We need to
write a similar function to construct an Either
from our timeStamp
parameter. We need to either parse a date or return an error string:
const eitherDate = timeStamp => {
const m = moment(new Date(parseInt(timeStamp)))
return m.isValid() ? Ok(m) : Fail(`Invalid date: ${timeStamp}`)
}
We just took the line from our original imperative implementation and
we use the .isValid
method to determine if we need to return Ok
or
Fail
. And now if we have a date we want to format it back to a
string. We can do that safely with .map
:
const parseTimestamp = timeStamp =>
eitherDate(timeStamp)
.map(dt => dt.format('YYYY-MM-DD HH:mm Z'))
And if we test it out:
> parseTimestamp(100)
definitions { r: 'definitions { r: '1969-12-31 19:00 -05:00' }' }
Let’s just do one small tweak to take that constant and make it a default parameter:
const parseTimestamp = (timeStamp, {format = 'YYYY-MM-DD HH:mm Z'} = {}) =>
eitherDate(timeStamp)
.map(dt => dt.format(format))
And now I think we’re good:
> eitherParseDate(100, {format: 'YYYY-MM-DD'})
definitions { r: '1969-12-31' }
> parseTimestamp(100)
definitions { r: 'definitions { r: '1969-12-31 19:00 -05:00' }' }
How does this compare to our original implementation? For one, our new, functional API is more explicit about what to do in the failure case. The user cannot forget to handle the failure case either! Let’s see what I mean:
> var old = convertUnixTimeStamp(userInput)
What is the value of old
if userInput
is undefined
?
Right: it is the string "Invalid date."
.
What is the value of old
if userInput
is a Number
?
Right: it is a string that is a parsed and formatted time stamp.
How do I know that when I’m calling convertUnixTimestamp
which kind
of result I have? Maybe I could try:
if (old === 'Invalid date.') {
console.log('Oops!')
} else {
console.log(`Got: ${old}!`)
}
Which is fine. I have an intermediate variable and an if
statement. Not bad.
My parseTimestamp
function makes you think about this case up-front
when you want to get a value out of the Either
, like we learned,
with .fold
:
const defaultTimestamp = parseTimestamp(userInput)
.fold(err => console.log(err),
result => console.log(result))
Which is the equivalent to the if-statement solution… but we can do
more here. The Fail
case could mean we need to return a default
timestamp in some cases. Or maybe our user needs to do more
intermediate transformations on the input before checking for a result
in .fold
. Or they need to compose it with other functions. All of
these scenarios are trivial with this pattern.
Whenever you see a pattern of conditionals in your code, especially
when those conditionals deal with user input, it’s a good use case for
refactoring your code to use Either
.