DEV Community

loading...

Whose afraid of the that big bad wolf?

John
・8 min read

Welcome to fantasy land, this post is about that big bad wolf, the monad. More specifically, about a monad called Maybe. There are other types of monads; for instance, the Either and IO monads which have been developed to deal with different circumstances within computer programs.

Why big bad wolf? Well, monads can be a bit scary; it's not, how would you say, idiomatic in JavaScript.

So this word monad, if you search the internet, is connected with phrases such as:

"Once you finally understand monads, you lose the ability to explain monads to others."

and...

"A monad is just a monoid in the category of endofunctors, what's the problem?"

It turns out that these comments about monads are, of course, jokes to make fun about the difficulty people have with explaining monads to others. You kind of get the feeling your in for a hard time with the monad, even before you start!

So, if you are venturing down that "rabbit hole" into the fantasy land of abstract data types(ADT's) then I hope that I can give you some idea about the concept of a monad without needing to dive into category theory, or telling you that you must learn Haskell before you can understand a monad.

In this post I hope that, for the newcomer(and let's face it, we are all newcomers at something), I can show you a "bridge" between your present JavaScript knowledge and the use of the Maybe monad in your code. The only prerequisite is that you are familiar with JS Array methods, mainly .map() and the JS Promise object; additionally, I assume you have used functional composition and currying using es2016 function expressions.

So what is a monad

A monad is an object that wraps a value in a context. Let's explain that in terms that a JS programmer should be familiar with.

Here is an analogy; an Array, as we know, is a collection of elements, like this:
[0, 1, 2, 3, 4, 5]

Here is an Array of only one element.


const  M  = [2]

M is a wrapped value; it has a context. The context is the Array. Array has a large number of methods that it provides.


//  Stay with me I just need to set up some functions to work with...
const log = console.log
const sq = n => n * n

// add - a curried version of the add function.
const add = a => b => a + b

// a new function made by partially applying the add function.
const add10 = add(10)

log(typeof add10)
// => function

// OK let's continue...
// we can "map over" this wrapped value ie. apply a function to the value
// to transform it to another value.

const result = M
    .map(add10)
    .map(sq)

log(result)
// => [ 144 ]

// [144], this result may not be what you want, it is still wrapped.
// So in JS, the way to get at the value is .pop()

log(result.pop())
// => 144

So what has the ability to .map given us? It has given us a way to transform a value by applying functions to the value and then, repeatedly, doing the same thing again. This is not function composition as such, but value transformation.

Incidentally, an object with a .map method is referred to as a functor. So an Array is a functor. Also, a monad is a functor; and more. At this stage if you are still not quite sure what I am going on about then this might help by giving a pictorial view of what is happening.

So what exactly did happened in the code above? These are the steps:

  • a wrapped value([2]) was mapped over by taking the value 2 out of the Array context and applying the function(add10) to the value

  • the result of the function application(12) was then wrapped back into the Array context

  • the next .map was treated in the same way unwrap 12, apply function sq, value is now 144, wrap it back into the Array.

Let's make a function, using a similar approach to the above, but operating on a String value.


const  exclaim  =  s  =>  s  +  '!'
const  exclaimWorld  =  val  =>
    // .of lifts the value into the Array
    Array.of(val)
      .map(s  =>  s.concat(' World'))
      .map(exclaim)
      .pop()



// ...and call it.

log(exclaimWorld('Dave\'s'))

// => Dave's World!

Well, this is all very fine and dandy, but what if that value is'nt there. Then what?


const  o  = {
  val:  "Hello",
  nil:  null,
  udef:  undefined
}

log(exclaimWorld(o.val))
// => Hello World!

// - OK, good it worked because the value passed in is a value.

// Now watch carefully...

log(exclaimWorld(o.udef))
// => TypeError: Cannot read property 'concat' of undefined.

BOOM! our program is dead, now we need that Maybe! We cannot do undefined.concat(value); this is the problem.

Yes, we need that Maybe, but first, let's look at one more analogy; this time using an object JS programmers should know about, the Promise object.

I will call this pMaybe to distinguish it as being derived using a Promise. Please note that this is just for training purposes to show the similarity of a Maybe to another common object in JS. We have seen that Array gives us one property of a Maybe; the ability to .map over a value applying whatever function to the value as required ie. Maybe is a functor. But fails to provide protection against undefined/null situations. The Promise, however, allows us to follow one of two paths that is normally called resolve and reject, but here I rename them Just and Nothing. Yes, I am slightly abusing the normal use case for the Promise, but it will hopefully aid our understanding.


const  pMaybe  =  val  =>
  new  Promise(
    (Just, Nothing) => {
      if (val  ===  undefined  ||  val  ===  null) Nothing(val)
      Just(val)
})

const pExclaimWorld = s =>
  pMaybe(s)
    .then(s  =>  s.concat(' World'))
    .then(exclaim)
    .then(s  =>  console.log(s))
    .catch((e) =>  console.log(`You passed in ${e} this is a likely cause of error!`))

pExclaimWorld(o.val)
// => Hello World!

pExclaimWorld('Brian\'s')
// => Brian's World!

pExclaimWorld(o.udef)
// => You passed in undefined this is a likely cause of error!


// to make our code more composable we need some curry! Here's a curry function

const  curry  =  f  => (...args) =>
  args.length >=  f.length
    ?  f(...args)
    :  curry(f.bind(undefined, ...args))


// a prop function
const  prop  =  curry((k, o) =>  o[k])

// Another example

const  add10ToAge  =  personO  =>
  pMaybe(personO)
    .then(prop('age'))
    .then(add(10))
    .then(result  =>  console.log(result))
    .catch(e  =>  console.log(`Nothing , check your input`))

add10ToAge({ name:  'Dinah', age:  14 })

// => 24


// Note: The results from the above will appear later in the output
// because of the effect of the Promise.

Now you see this time our code does not err. The potential threat of undefined on our code is mitigated by the .catch() and a undefined or null is handled with a friendly response. This has a severe limitation though and that is the undefined/null checking only happens on the initial lifting of the value into pMaybe. If a null or undefined is generated as a result of any of the following .then calls it will not produce a Nothing ...and that's not good. To see this in action change the 'age' prop to 'ag', which is not a property on the passed in object. It now will return Just(NaN). What we really wanted is Nothing, indicating the code failed due to undefined.

By now I am hoping that you are getting the idea about the Maybe monad. I have tried to relate it to something that a JS coder should already know. But, fear not if you are still not comfortable with it, there are still more examples to come.

Now we know what problem the Maybe is designed to solve; the absence of a value(ie. undefined or null which is a common cause of bugs in JS code). We have also discovered that a Maybe has the properties of an Array(the ability to map) and the branching ability of a Promise, but they both fall short. So, let's go for it. Let's do an actual Maybe in JavaScript and here it is...

Maybe for safer code in JavaScript


const  Maybe = (function () {
  const  Just  =  function (x) { this.x =  x; };
  Just.prototype.map  =  function (fn) { return  Maybe.of(fn(this.x)) };
  Just.prototype.chain  =  function (fn) { return  fn(this.x) };
  Just.prototype.toString  =  function () { return  `Just(${this.x})` };

  const  Nothing  =  function () {};
  Nothing.map  = () =>  Nothing;
  Nothing.chain  = () =>  Nothing;
  Nothing.toString  = () =>  'Nothing';

  return {
    of: (x) =>  x  ===  null || x  ===  undefined  ? Nothing : new Just(x),
    lift: (fn) => (...args) =>  Maybe.of(fn(...args)),
    Just,
    Nothing
  };
})();

// Thanks to Edd Mann
// https://eddmann.com/posts/maybe-in-javascript/

So, in less than twenty lines of code a Maybe is laid bare here in vanilla JS. Study it carefully; it is a thing of beauty! This is still not a fantasy-land compliant Maybe, however, it is a step towards making our code more robust and still composable. Later, (perhaps in another post), we will swap to using the folktale Maybe, if you think that might help you further.

Right, how does it work?

You can see that it has a Just function for handling the happy path(the presence of a value) and a Nothing function to handle the case when a value is absent. Mapping when the Maybe is routed down the happy path causes a provided function to be applied to the value passed into the Maybe; whereas no such function application occurs when mapping on the Nothing path. This is where the protection from errors due to undefined/null happens. Finally, the Maybe returns an object so that the user can interact with it.

Also, please note, that each time we .map we rewrap the result by doing Maybe.of which ensures undefined/null checking continues to happen.

  • .of() is usd to lift a value into the Maybe and initiates a null/undefined check.
  • .lift() is used to lift a fuction and its arguments into the Maybe.

Better than explaining it line by line, let's see how we can use it to solve the problem of the exclaim function above. Let's make a safeExclaim function.


const  safeExclaimWorld  = val =>
  Maybe.of(val)
    .map(s  =>  s.concat(' World'))
    .map(exclaim)
    .chain(x  =>  x)
    .toString()

log(safeExclaimWorld('Bob\'s')
// => Bob World!

log(safeExclaimWorld(o.udef)
// => Nothing

Note: that this time we don't get a TypeError. That's good! The Maybe justifies its existence.

Notice how similar the exclaim and the safeExclaim functions are. In particular you should note the following points:

  • As you know, a value is lifted into the Maybe using .of(); you can liken this to Array.of()

  • Value transformation is provided using .map(); in a similar fashion to Array .map()

  • Now we come to the line .chain(x => x); this gets our value out of the Maybe, similar to .pop() with the Array version.

Incidentally, the anonymous function x => x is commonly referred to as the identity function(const identity = x => x).

.chain flattens the Maybe and out pops the value.

  • .chain() needs to be passed a function to work. In the above situation we don't want to transform the value, so hence identity is used.

More examples

Here's an example to show another way Maybe can be used

The Array method .find() will find the first occurence of an element in an array or return undefined. Let's imagine we have an array of bears and want to find a specific bear in a safe manner.


const bears = ['Black', 'Grizzly', 'Kodiac', 'Polar', 'Spectacled', 'Sloth']
const find = curry((f, xs) =>  Array.prototype.find.call(xs, f))

// .lift() - lifts a function and its arguments into a Maybe

// safeFind : Array -> Any -> Any|Object

const safeFind = curry((xs, val) =>
  Maybe.lift(find)(el => el === val, xs)
    .chain(x  =>  x))

console.log(safeFind(bears, 'Kodiac'))
// => Kodiac

console.log(safeFind(bears)('Polar'))
// => Polar

console.log(safeFind(bears)('Big') )
// => a Nothing object

Another example demonstrates that safeFind will work for other types of values.

const  integers  = [34, 72, 56, 82, 94, 27, 11, 45, 42, 77, 90, 55]

const  finder  =  curry((coll, x) =>  safeFind(coll, x))
const  intFinder = finder(integers)

console.log(intFinder(42)
// => 42

console.log(intFinder(94))
// => 94

console.log(intFinder(423))
// => a nothing object

At this point I encourage you to copy this code, play around with it and make up your own examples. Doing this will help your understanding far more than just reading this article.

Here is the code for this article runkit

Thanks, maybe I'll be back with more!

If this has helped your understanding of the Maybe then please leave a comment. Likewise, if I've caused you confussion then your feedback will also be valuable for further edits.

Discussion (0)