This is part three of a series of posts where I show you some patterns and tricks for improving every day code with functional programming. If you haven’t seen the first post go read it first. You might not understand everything in this post if you’re just starting out in the world of functional programming!
The Humble Burrito
Why does the myth that Monads are hard continue to persist? They’re a useful tool that can clean up any code base and make hard problems seem easy. The essential problem monads address in computer programs is composition. Do you see a theme here? Monads in particular are great for structuring nested computations.
If you’ve ever written a piece of code that takes some input or state and then proceeds through a chain of transformations you could’ve benefitted from monads. The reason is that they compose well together and let you control where, and sometimes when, those steps happen… from the outside.
It sounds a little abstract so let’s jump into our example:
const users = [
{
id: 1,
profile: {
first: "John",
last: "Doe"
}
},
{
id: 2,
profile: {
first: "Jane",
last: "Doe"
}
},
{
id: 3
},
{
id: 4,
profile: {
fst: "Foo",
last: "Bar"
}
},
{
id: 5,
profis: {
first: "Derp",
last: "Dop"
}
}
]
Here we’re defining a data strucure to work with to illustrate a simple example where we can use functional programming techniques to make our code more beautiful, maintainable, and resistant to errors.
The problem we sometimes face as programmers is unreliable input. We would like it if our data was always formatted precisely but sometimes scripts encounter strange behaviours when processing large amounts of data or sometimes it’s plain human error when entering it. Either way we’d like our code to not break if we encounter a bad input. It would also be nice if we could re-use that code in case the specifications change in the future.
So let’s write a function to get the full name of a user object from this list. We’ll write it in a straight-forward style:
const getFullName = user => {
const first = user.profile.first
, last = user.profile.last;
return `${first} ${last}`
}
You may have code that looks like this in your code base or have come across it before somewhere else. Of course this is pretty naive since our third user in the list doesn’t even have a profile.
const getFullName = user => {
const profile = user.profile
, first = profile ? profile.first : undefined
, last = profile ? profile.last : undefined;
return first && last
? `${first} ${last}`
: `unknown`
}
Now we’re getting a little more defensive. Once you’ve been programming in Javascript for a while you learn to recognize where accessing an attribute from an object that could be undefined
can bite you. This version isn’t too bad.
And if we want to get the whole list of names
users.map(getFullName)
Right on! Problem solved!
Enter the Monad
Not so fast. This is just a trivial example. You’ve seen much larger functions where you’ve had to use these defensive techniques to avoid failing your unit tests. You’ve let a good chunk of your code base be taken up by testing for undefined
and null
. There are if
statements everywhere. Is there a better way?
I’m glad you asked. First some preliminaries. I would like to direct you to Professor Frisby’s course on egghead.io. However if you don’t have the time to go through that and jump back here I’ll give you the cliff’s notes.
Did you see map
before? Isn’t it wonderful? We love chaining things together like that in Javascript. We can chain map together like that all day long to transform some values.
[1, 2, 3]
.map(x => x + 1)
.map(x => x * x)
In fact we don’t really give it a whole list if we just want to transform a single value. The important thing is that we put those values in a box. When we do that we get the map
method on that box.
Monads are like boxes. They are a data type with a value inside and some methods for interacting with that value. That’s it.
To illustrate our point let’s create a monad right now:
const Box = x => ({
x,
map: f => Box(f(x))
})
This is enough to let us do our map
thing again:
Box(4)
.map(x => x * x)
.map(x => x + 1)
We’ll need a few more methods in our interface to make a true monad type. However the point of me showing you box is so that you understand the concept and begin to realize that the dense terminology is just terminology. You can understand the concepts. The terminology will come with time.
So the full list of methods our Box
would have to implement is thankfully enumerated in the Fantasy Land Spec.
For our purposes in the rest of this article we’ll rely upon an implementation of a monad called, option. This type represents a value that may not exist. There are two possible instances you can create from the option
type: Some
or None
. Either you have some value or you have none. To play off of box let’s look at how you could implement your own option monad:
const Some = x => ({
x,
map: f => Some(f(x)),
chain: f => f(x), // this is new
inspect: () => `Some(${x})` // so we can see our value
})
const None = () => ({
map: f => None(),
chain: f => None(),
inspect: () => `None()`
})
We added a few things here. We added the chain
method. What does that do? Well we saw how map
applies a function to the value in our box. What if that value in this case is another option
type?
const maybeUserProfile = user =>
'profile' in user
? Some(user.profile)
: None()
const maybeProfileEmail = profile =>
'email' in profile
? Some(profile.email)
: None()
// and we can chain these together like so...
const maybeUserEmail = user =>
maybeUserProfile(user)
.chain(maybeProfileEmail)
maybeUserEmail({profile: {email: 'foo@bar'}}).inspect() // Some('foo@bar')
maybeUserEmail({profile: {foo: 'bar'}}).inspect() // None()
We use .chain
when our function f
returns another box. This lets us pass values along… in a chain. Get it? Not all of the terminology is dense.
Now let’s talk about None
for a second. It’s the same type as Some
… it’s an instance of option
. It represents the absence of a value. When we have multiple instances of a single type we call it a, sum type. option
is a sum type.
What’s really cool about monads is how they compose together. We can map over a Some
and some stage of our computation might return a None
and that’s okay! We don’t have to explicitly check for it; it’s all passed through.
Let’s look at how this plays out with our library which implements a complete option
type:
const option = require('fantasy-maybes')
const R = require('ramda')
const maybeUserProfile = user =>
'profile' in user
? option.Some(user.profile)
: option.None
const profile = maybeUserProfile({id: 1,
profile: {first: 'Foo',
last: 'Bar'})
const fullName = profile
.map(p => `${p.first} ${p.last}`)
.getOrElse(`Unknown`)
Here we have something that is starting to look like how you’d write our original example using monads. It’s still not complete. We still need to handle the case where the profile object isn’t what we expect. However this is a good start.
First notice our .getOrElse
method. We’re treating monads like boxes for now. This method is the way our library lets us get a value out of the box once we’re done. Notice anything else that seems interesting about this code?
What does maybeUserProfile
return? It’s not a profile!
It returns an instance of our option
type. It can return either a Some
instance or a None
instance. These instances have the same methods that make them monads so we can treat them just the same in our code. We can call .map
and friends freely without worrying if the object passed into maybeUserProfile
had the right property.
It also means that we can build up our computation dynamically and only evaluate the result when we want to. Pay attention to this when you’re exploring monads: it’s one of their most salient benefits.
One last observation: the caller of maybeUserProfile
gets to determine the final value in the case where None
is returned. You might hear the term, inversion of control when people talk about monads. This is one way that happens. We’ll see more about this as we expand on our example.
Maybe Ramda
Ramda is a library we’ve used a number of times in this series. It’s a great library filled with helpful functions for combining and creating new, interesting functions. We like composing small things to make bigger ones. Ramda is great.
However when working with our new-found monad powers we may find Ramda lacking. There are many functions that return undefined
and often we’d rather get back an instance of None
.
Fortunately Ramda is easy to extend with our new-found powers. We’re going to quickly create a module to help us use option
types with Ramda.
// some-ramda.js
const option = require('fantasy-maybes')
const R = require('ramda')
const maybeValue = v => v !== null && typeof v !== 'undefined'
? option.Some(v)
: option.None
const maybeIndex = v => v !== -1
? option.Some(v)
: option.None
const maybeF = f => R.compose(maybeValue, f)
const maybeI = f => R.compose(maybeIndex, f)
module.exports = {
find: maybeF(R.find),
findIndex: maybeI(R.findIndex),
findLast: MaybeF(R.findLast),
findLastIndex: MaybeI(R.findLastIndex),
prop: maybeF(R.prop),
...
}
There are two kinds of functions we want to wrap in Ramda: functions that may return a value and functions that may return an index. We create functions to return an instance of option
for those cases and then create a helper combinator to take any function, f
, and wrap its return value with our option constructors.
We then enumerate through the list of ramda functions that get a value or index and wrap them with our helpers. Now we have monads!
Some Users
Remember our list of users?
const users = [
{
id: 1,
profile: {
first: "John",
last: "Doe"
}
},
{
id: 2,
profile: {
first: "Jane",
last: "Doe"
}
},
{
id: 3
},
{
id: 4,
profile: {
fst: "Foo",
last: "Bar"
}
},
{
id: 5,
profis: {
first: "Derp",
last: "Dop"
}
}
]
Let’s use our new powers to re-write our original example. First let’s write a function that takes a user object and returns Some profile
:
const M = require('some-ramda')
const userProfileM = user => M.prop('profile', user)
That’s good. We’re getting used to monads now so we’re going to abbreviate our maybeSomething
pattern to somethingM
instead. We can get a profile from a user object and if there isn’t one we’ll just get None
. Now we can write a function to get the full name of the user from their profile without worrying about whether the profile is there or not.
const M = require('some-ramda)
const userFullNameM = user =>
userProfileM(user)
.chain(p => M.prop('first', p)
.chain(first => M.prop('last', p)
.map(last => `${first} ${last}`)))
Whoa! We haven’t used chain
that much yet… this looks like a lot to take in. Don’t despair! You know what chain does, remember? We use map
to apply a function to the value in our box. We use chain
if our function returns another box. Since prop
returns an option
type we need to use chain
.
This looks a little bit harder to follow than our original implementation of getFullName
.
const getFullName = user => {
const profile = user.profile
, first = profile ? profile.first : undefined
, last = profile ? profile.last : undefined;
return first && last
? `${first} ${last}`
: `unknown`
}
What has all of these nested .chain
calls given us?
Remember when we talked about inversion of control? What can we observe about the value of getFullName
?
It’s a string. A string that we, the caller, cannot change. If we want to know that the lookup failed we’ll have to parse this string… and that’s not a very good interface. Our monads let our callers decide how to handle the None
instance.
You might say that we could return a sentinel value from getFullName
. How well does that compose? Or maybe a callback?
Our userFullNameM
function returns an option
instance which means we can compose it together with other options (and with a little help, even other monads!). And we get to build our computation dynamically and only evaluate it when we choose. If we chose to return a sentinel value… well we’d be inventing our own protocols and abstractions. Our users might know monads and monads have laws so let’s stick with them.
Let’s try searching for a user in our list:
const M = require('some-ramda')
const findUserM = (users, id) =>
M.find(R.propEq('id', id), users)
const findUserNameById = (users, id) =>
findUserM(users, id)
.chain(userFullNameM)
There’s a lot more we could do. This is great!
Conclusion
This article was a little longer than the others. That’s okay. We had to cover a little more ground. However we discovered a new power: monads.
So what patterns can we look for in our code, today, that we can replace with this technique to make our code more beautiful, maintainable, and bug-resistant?
Look for code that needs to transform or extract data from nested structures. That’s a great use-case for our option
type. As we’ve seen it lets us eliminate extraneous conditionals from our code and forget about constantly checking for undefined
and null
. Start at the bottom and slowly work your way back to the top. Write some helper libraries along the way to wrap functions into option
types.
But don’t stop there! There are other monads besides option
which you can use to structure your code and make it better. If you’re already using Promise
then you’ve actually been using a kind of monad! Keep going and learn about some other ones like either
, future
, state
, etc. They’re great!
Happy hacking.