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.
Parsing User Input
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.
Either
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
.