Understanding the Maybe Type
Imagine a scenario where invoking a function could fail if an argument passed to the function is the wrong data type (i.e. a string instead of a number), null
, or undefined
. You can guard against errors by checking your input with typeof
or checking for null
or undefined
. This adds code and can make reading and refactoring more difficult. This sort of scenario is exactly what the Maybe
type can help us avoid. The Maybe
offers a way to encapsulate the safety that these conditionals bring without making the code too “noisy”.
In this article, we’ll build up to using Maybe
in a small example before we expand on our understanding to use the Maybe
in more ways in the upcoming articles in this series.
The Problem
We’ll start with some small utility functions to use in the upcoming examples. In a utils.js
file, we’ll start with an inc
function that increments a provided number.
const inc = (n) => n + 1
If we run the inc
function passing in a number, we’ll get the expected result of that number + 1.
import { inc } from './utils'
const input = 2
const result = inc(input)
console.log(result) // 3
The problems start to arise when we pass in a non-numeric value, such as the string '8'
import { inc } from './utils'
const input = '8'
const result = inc(input)
console.log(result) // '81'
In this case, the string on the left hand side of the +
operator led to the 1
inside inc
to be coerced to a string and then it was concatenated onto our supplied value.
If we pass undefined
instead, we’ll end up with NaN
as a result.
import { inc } from './utils'
const input = undefined
const result = inc(input)
console.log(result) // NaN
And finally, if we pass null
, we’ll end up with 1
as a result.
import { inc } from './utils'
const input = null
const result = inc(input)
console.log(result) // 1
All of these are problematic in their own way. Ending up with a NaN
or a string where you expect a number is sure to lead to an error when you use this value for future operations. The result of 1
for the null
input could be fine, but could lead to an even stranger bug when your math is off, but there is no error. Maybe you think your input is making it into the function, but it’s becoming null
at some point. This is the sort of bug where you might not think to look for that, and you’ll look in other places to see why the math is off.
Sure, this example is small and it probably wouldn’t take long to work this out, but in the context of this being a single step in a larger operation, this sort of thing can eat up valuable time and patience.
Adding Some Guards
Let’s start with a standard approach, we’ll add some conditional logic in here and if our input is invalid, we’ll default to 0
for our return value.
Let’s start off by putting this at the point where we invoke the inc
utility. We’ll add in a ternary and if the typeof
input is a number, we’ll pass it to inc
and if not, we’ll skip the invocation and return our default value of 0
.
import { inc } from './utils'
const input = 2
const result = typeof input === 'number' ? inc(input) : 0
console.log(result) // 3
We don’t want to do this every time we invoke this function, that’ll make our code really noisy and we could easily forget to add it in all the places. We could move this logic into the utility function, but that only works if we always want the same default and if we have direct control over the code. Instead, let’s take a look at how Maybe
can help us out.
Bringing in the Maybe Type
For our examples, we’ll be using the Maybe
type as implemented by the Crocks ADT library. You can install it into a project via npm
with
npm install crocks
Once it’s installed, you can import Maybe
with
import Maybe from 'crocks/Maybe'
With crocks installed and the Maybe type imported, let’s look at the basics.
Nothing & Just
A Maybe
is a union type that can be one of two underlying types. Maybe
can be a Nothing
which means there is no value, or a Just x
(read “Just of x”) that holds a value (x
). The type can be represented as:
// Maybe = Nothing | Just x
If we want a maybe that holds a value (a Just x
) we can construct that with the Just
constructor method on Maybe
.
const input = Maybe.Just(2)
The value of input
will now be a Just
that holds the value of 2
. We can see this by logging out input
and calling toString()
.
const input = Maybe.Just(2)
console.log(input.toString()) // Just 2
By constructing this Just
, we’ve wrapped our 2
away inside this container. This means we can’t simply pass it into the inc
utility since it expects a number. Instead, what we’ll do is use methods available on the Just
type. In order to invoke a function on the contained value, we’ll use the map
method.
import Maybe from 'crocks/Maybe'
import { inc } from './utils'
const input = Maybe.Just(2)
const result = input.map(inc)
console.log(result.toString()) // Just 3
You can imagine that map
here unwraps the value from the Just
, passes it to the function (inc
in this case), then wraps the value again so we can continue to operate on the value with the safety of the Maybe
intact.
At this point, you might be wondering where the safety is. We’ll get there, but first let’s talk about the Nothing
piece of this union type. Instead of constructing a Just
with a value for our input, let’s swap that out with a Nothing()
constructor method.
import Maybe from 'crocks/Maybe'
import { inc } from './utils'
const input = Maybe.Nothing()
const result = input.map(inc)
console.log(result.toString()) // Nothing
Our result here is a Nothing
. Inside a Maybe
instance, a Just
will invoke the function passed to map, a Nothing
will skip it, avoiding any potential errors or unintended results.
Let’s see this in action. Let’s update the function passed to map
so we can log when it is invoked. We’ll add an arrow function, console.log and pass the argument through to inc
so it will log to the console and continue to work as expected.
import Maybe from 'crocks/Maybe'
import { inc } from './utils'
const input = Maybe.Nothing()
const result = input.map((n) => console.log('running map', n) || inc(n))
console.log(result.toString()) // Nothing
Since console.log
returns undefined
, that ||
will evaluate the expression on the right and result in inc
being called and its result being returned in the map
.
When we run this, we’ll get the Nothing
result and should see no output from the function in map
. Now let’s replace that Maybe.Nothing()
with Maybe.Just(3)
.
import Maybe from 'crocks/Maybe'
import { inc } from './utils'
const input = Maybe.Just(3)
const result = input.map((n) => console.log('running map', n) || inc(n))
console.log(result.toString()) // Just 4
This will result in a Just 4
result as well as running map 3
being output in the console when map
runs.
Creating a safeNum
Function
Now that we’ve seen how things like map
behave in a Maybe
based on the underlying type, the next step is to take an input
that could be valid or invalid and create an instance of the correct underlying type. For this example, we’ll make a safeNum
function.
We’re going to duplicate some logic we used earlier by checking our data type with typeof
but we’ll use this as a constructor for a Maybe
this time.
const safeNum = (val) =>
typeof val === 'number' ? Maybe.Just(val) : Maybe.Nothing()
Now we can use safeNum
to create our input
value.
import Maybe from 'crocks/Maybe'
import { inc } from './utils'
const safeNum = (val) =>
typeof val === 'number' ? Maybe.Just(val) : Maybe.Nothing()
const input = safeNum(3)
const result = input.map((n) => console.log('running map', n) || inc(n))
console.log(result.toString()) // Just 4
If we pass in a number, we’ll see the console.log
from our map
and a result of Just 4
.
If we instead pass in a non-number value like a string, null
, undefined
, []
, or {}
, we will not see the console.log
and the result will be a Nothing
.
import Maybe from 'crocks/Maybe'
import { inc } from './utils'
const safeNum = (val) =>
typeof val === 'number' ? Maybe.Just(val) : Maybe.Nothing()
const input = safeNum(null)
const result = input.map((n) => console.log('running map', n) || inc(n))
console.log(result.toString()) // Nothing
Wrap Up
This is just the start of being able to take advantage of the Maybe
type. In upcoming articles, we’ll dive into more methods and scenarios where having our values wrapped up in a Maybe
can help keep our code safer and more predictable.