Create a Maybe with a safe Utility Function

5 min read

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.