Create a Maybe with a safe Utility Function
In the previous article in this series, we looked at the Maybe type and how we could use it to abstract guards out of our code and into the type. We first constructed the Maybe.Nothing
and Maybe.Just
directly, then created a very specific utility function to construct them for us. In this article, we’ll create a more generic utility to understand it first and then we’ll look at one that is built into the crocks library.
Setup
We’re going to start off with a couple utility functions, inc
and toUpper
. And some sample code to compare approaches. We’re using a Maybe
type to wrap a numeric value and invoking inc
on that by using the map
method on our Maybe
instance. For the string value, we’re just calling the utility directly, without the safety of the Maybe
type.
import Maybe from 'crocks/Maybe'
import { inc, toUpper } from './utils'
const safeNum = (val) =>
typeof val === 'number' ? Maybe.Just(val) : Maybe.Nothing()
const inputN = safeNum(5)
const resultN = inputN.map(inc)
const inputS = 'test'
const resultS = toUpper(inputS)
console.log(resultN.toString()) // Just 6
console.log(resultS) // TEST
Of course, if our input to the toUpper
utility isn’t a string, we’ll end up with errors. So let’s take a similar approach for wrapping our string input in a Maybe
. We’ll start by creating a safeString
function that behaves just like the safeNum
function, only for strings.
Wrapping the String in a Maybe
const safeString = (val) =>
typeofval === 'string' ? Maybe.Just(val) : Maybe.Nothing()
If we have a string, we wrap it in a Just
, otherwise we end up with a Nothing
.
Now we can use that to construct our input.
-const inputS = 'test'
+const inputS = safeString('test')
Then, in order to use the toUpper
function, we’ll map over it.
-const resultS = toUpper(inputS)
+const resultS = inputS.map(toUpper)
Then we’ll update our output to give us just the string representation of our result.
-console.log(resultS) // "TEST"
+console.log(resultS.toString()) // Just "TEST"
Now we have 2 utilities that do essentially the same thing with a slight variation in the exact check being done. Let’s see if we can make this a little more generic and get some reuse without hurting clarity.
A More Generic safe
Function
The major difference in our safeNum and safeString functions comes down to the conditional used to decide between a Just
and a Nothing
. Let’s first extract that difference into some straightforward predicate functions.
const isNumber = (val) => typeof val === 'number'
const isString = (val) => typeof val === 'string'
Now we can use those in our safeNum
and safeString
functions.
const safeNum = (val) => (isNumber(val) ? Maybe.Just(val) : Maybe.Nothing())
const safeString = (val) => (isString(val) ? Maybe.Just(val) : Maybe.Nothing())
That’s a start, but we can make this better. Let’s define a function called safe
and we’ll make its first argument a predicate function. We’ll apply that predicate to our value, and return the appropriately constructed Maybe
instance.
const safe = (predicate, val) =>
predicate(val) ? Maybe.Just(val) : Maybe.Nothing()
Then we can update our usage by applying safe
with the corresponding predicate in place of safeNum
and safeString
.
-const inputN = safeNum(5)
+const inputN = safe(isNumber, 5)
const resultN = inputN.map(inc)
-const inputS = safeString('test')
+const inputS = safe(isString, 'test')
const resultS = inputS.map(toUpper)
console.log(resultN.toString()) // Just 6
console.log(resultS.toString()) // Just "TEST"
Now we can safely remove our safeNum
and safeString
functions and use the new safe
function in their place. At this point, our full example will look something like this.
import Maybe from 'crocks/Maybe'
import { inc, toUpper } from './utils'
const isNumber = (val) => typeof val === 'number'
const isString = (val) => typeof val === 'string'
const safe = (predicate, val) =>
predicate(val) ? Maybe.Just(val) : Maybe.Nothing()
const inputN = safe(isNumber, 5)
const resultN = inputN.map(inc)
const inputS = safe(isString, 'test')
const resultS = inputS.map(toUpper)
console.log(resultN.toString()) // Just 6
console.log(resultS.toString()) // Just "TEST"
Using Built In Utilities
We built all of this just to see how it works, but because these are pretty standard use-cases for the Maybe
type, we can rely on the crocks library to provide utilities like these for us.
Let’s update the example to remove our utilities in favor of the Crocks provided ones.
-import Maybe from 'crocks/Maybe'
+import safe from 'crocks/Maybe/safe'
+import isNumber from 'crocks/predicates/isNumber'
+import isString from 'crocks/predicates/isString'
import { inc, toUpper } from './utils'
-const isNumber = (val) => typeof val === 'number'
-const isString = (val) => typeof val === 'string'
-const safe = (predicate, val) =>
- predicate(val) ? Maybe.Just(val) : Maybe.Nothing()
const inputN = safe(isNumber, 5)
const resultN = inputN.map(inc)
const inputS = safe(isString, 'test')
const resultS = inputS.map(toUpper)
console.log(resultN.toString()) // Just 6
console.log(resultS.toString()) // Just "TEST"
And with a couple quick import changes and some deleted code, we have our existing functionality intact without having to provide the utilities ourselves.