Torsten Müller

Implementing the Option Monad in JavaScript

published Dec 23rd, 2019

In a previous post, I wrote about pagination using immutable state and praised it as the solution to corruption of the state through third parties. In this post I’m going to write about how to create a monad that implements the Option type, a functional programming paradigm that deals with the potential absence of a value in a variable, without the ubiquitous and unpleasant null checks throughout our code.

The problem addressed here

As every programmer knows, it’s common that a computation throws an error or returns an unexpected result, such as Infinity, with invalid or not previously considered input. So the usual, object oriented solution is to check for those conditions explicitly.

A monad is a construct which encapsulates a value of a given type and provides a set of methods that can be applied to the contained value. The result of that computation is then encapsulated in another instance of the same monad type. A monad is thus an example of an immutable data structure.

In this case, I’m going to discuss the Option monad, which has two subtypes, a Some of type T which encapsulates a value, and a None which indicates that no value is present. The behavior of these two instance types is that a None always returns a None, whereas a method supplied to a method on the Some is applied to the contained value (The name Some and None are influenced by the Scala programming language). Here is an example of how an Option monad would be used, using TypeScript for this example:

option-implementation.ts
const original = Some(84);
const result: Option<number> = original.map( (x: number) => x / 2);
const hitchhiker: Option<string> = 
    result.map( (x: number) => x + ' is the answer to all questions');

In this basic use example, a Some is created in line 1, and its map() method is invoked in line 2, to create another Option of type number. The last two lines generate a string in yet another Some instance from the value calculated in line 2.

In this basic example, the Option is somewhat superfluous, but consider this (omitting the types for clarity here):

option-implementation.ts
const original = Some(42);
const result = original.map( (x) => throw 'some exception');
const hitchhiker = result.map( x => x + ' is the answer to all questions');

Without an Option, we’d have to check for exceptions or other errors after each step of our computation, but the statement in line 2 will return a None to indicate the absence of a valid value. Since the interfaces of Some and None are identical, we can keep setting up our computation and not worry about errors, until we reach the end of our computation.

The Option subtypes None and Some

The meaning of these two objects are quickly described: The None monad represents the absence of a value, but provides the same interface as the Some type, which does contain a value of a certain type. This allows the programmer to not have to worry about whether a computation succeeded and to simply proceed as if everything worked swimmingly. In the end, the value can be extracted. or a default value supplied, in case of an error occurring somewhere.

The functionality of our Option monad

In this example, I am using three JavaScript functions to represent the three monads of the Option type: Option, None and Some. The code for the Option looks like this:

option.js
let Option = function (expression) {

  if (typeof expression == 'function') {
    try {
      return new Some(expression());
    } catch (e) {
      return new None();
    }
  } else {
    if (expression || expression === 0 || expression === false || expression === '') {
      return new Some(expression);
    } else {
      return new None();
    }
  }
};

The functionality of this function consists entirely of creating instances of either a Some or a None, depending on the parameter passed to it. If we pass a function, the constructor tries to execute it and return the proper Option, in the case we pass something other than a function, we simply instantiate and return the corresponding instance from this constructor. Since this type only creates instances of other monads, it does not contain a value or methods to manipulate a contained values itself.

Compare this comparatively simple constructor with the one for the Some option:

option.js
const Some = function(expression) {

  let value;

  if (this instanceof Some) {
    if (typeof expression == 'function') {
      try {
        let evaluated = expression();
        if (evaluated || evaluated === 0 || evaluated === '') value = evaluated;
        else return new None();
      } catch (e) {
        return new None();
      }
    } else {
      if (expression || expression === 0 || expression === false || expression === '') value = expression;
      else return new None();
    }
  } else {
    return new Some(expression);
  }

  this.getOrElse = function(defaultValue) { return value; };
};

There are a few things that stand out:

  1. On line 3, it defines a property which will store the value of the monad. As it is defined using let, it won’t be accessible from outside, and thus is hidden from manipulation.
  2. The if on line 5 serves to allow a programmer to create an instance of this object without using new, as that instantiation occurs in the else clause on line 19.
  3. We can pass a function to the constructor as a parameter (line 6), which will then be evaluated to generate the value stored in the monad.
  4. On line 22, the getOrElse method is defined, which returns the stored value, since this is a Some. A method by the same name is defined on a None, but will return the passed defaultValue instead. This method serves as an accessor to the private value for all other methods, which will be defined on the object’s prototype. Because of the need to access the value of the Some, it needs to be defined here to allow access to the private value property.

Since we don’t have direct access to manipulate the value stored in our Option, we need to provide a set of methods with which we can manipulate the value stored within the Option. In particular, I will implement the following methods:

name functionality
.getOrElse() Returns the value of the Option, or, for None, the value provided to the method
.map() transform the value
.filter() returns Some with value if filter method is true, None otherwise
.flatMap() Will apply the provided function to the value and will unpack a returned Option type to avoid nested Options
.flatten() Recursively unwraps nested Options and returns a Option<T>
.isEmpty() Returns a boolean indicating whether the Option instance contains a value
.forall() Applies a provided predicate to the value and returns true or false

Modifying the contained value

The methods map(), flatMap() and in the wider sense flatten() modify the contained value through application of a method that takes the stored value as input and returns a value, which will be wrapped in a new Option instance. Here is how they are implemented:

option.js
Some.prototype.map = function(fct) {
  try {
    const result = fct(this.getOrElse());
    return Some(result);
  } catch(e) {
    return None();
  }
};

Some.prototype.flatMap = function(fct) {
  try {
    const applied = fct(this.getOrElse());
    if (applied instanceof None || applied instanceof Some) return applied;
    else return new Option(applied);
  } catch (e) {
    return new None();
  }
};

The implementation for .map() is pretty straightforward: We call the getOrElse() method on the instance of Some to retrieve the current value, then apply the passed function to that value. We finally wrap the received value in a new Option instance. If we encounter an exception anywhere along the way, we return a None.

The same general functionality is used in flatMap(), except that the received value is returned directly if it is an Option type, otherwise, it is wrapped in an Option. This implementation avoids nesting Option types, but takes care of only one level of nesting. If we have a value that is deeply nested in Options, you can use the flatten() method, which goes through a nested set of Options and returns the innermost one. For example:

Some(Some(Some(Some(9)))).flatten()  --> Some(9);

Its implementation relies on recursion and thus can in (rare) circumstances cause a stack overflow, if Options are nested too deeply. Here is the implementation of the flatten() method:

option.js
Some.prototype.flatten = function() {
  function flattenOption(opt) {
    if (opt.constructor.name === 'None') return new None();
    else if (opt.constructor.name !== 'Some') return new Some(opt); 
    else return flattenOption(opt.getOrElse(new None()));
  }
  return flattenOption(this.getOrElse('None'));

This implementation shows that if the Option is a None, we simply return a new instance of None. The recursion happens in line 5, after we check whether the entity’s constructor indicates that it’s not a Some (line 4). If it not an instance of Some, we have arrived at the contained value and return that value wrapped in a Some. Otherwise, we proceed to recursively call flattenOption() by passing the contained value on line 5, which removes one level of nested Some at a time.

This implementation is an example of tail recursion, which would allow a compiler to replace successive recursions, i.e. function calls, through a for loop and thus avoid the danger of stack overflows. Alas, JavaScript does not supply this optimization, but it’s a good habit to develop for functional languages such as Scala, which do support tail recursion.

Filtering or providing defaults for contained value

Another method that is common in functional programming is filter(), which is commonly used on List instances to remove list items that don’t fit the predicate function. For an Option, which contains a single value (which might be an instance of a List), we simply ascertain that a particular condition applies. Its implementation looks like this:

option.js
Some.prototype.filter = function (fct) {
  try {
    if (fct(this.getOrElse())) return this;
    else return new None();
  } catch (e) {
    return new None();
  }
};

So in short, we return the Some instance if the predicate returns a truthy value, otherwise, or in the case of an exception, we return an instance of None. This leaves us with a few methods that provide state information about the instance value:

option.js
Some.prototype.isEmpty = function() {
  return false;
};

Some.prototype.orElse = function(fct) {
  return this.getOrElse(fct());
};

Some.prototype.forall = function(fct) {
  try {
    return !!fct(this.getOrElse(''));
  } catch (e) {
    return false;
  }
};

isEmpty() indicates, whether the instance contains a value. For a Some this is always false, so we can simply return the boolean value. forall() takes a function as argument and applies that function to the contained values. The function should return a boolean, but just to make sure, the implementation here coerces the returned value to a boolean to match the method’s signature. If the method passed to forall() throws an exception, we return false, as the precondition does not apply.

The implementation of the corresponding None type would be very simple and look like the following:

option.js
let None = function() {

  if (!(this instanceof None)) {
    return new None();
  }

  this.getOrElse = (defaultValue) => defaultValue;
};

None.prototype.map = function(fct) { return this };
None.prototype.flatMap = function(fct) { return this };
None.prototype.filter = function(fct) { return this };
None.prototype.isEmpty = function() { return true };
None.prototype.orElse = function(fct) { return fct() };
None.prototype.flatten = function() { return this };
None.prototype.forall = function(fct) { return false };

Here, we simply return a None from the constructor and define the getOrElse() method to return the passed parameter value. All the methods on the Object will simply return itself or the appropriate, hard-coded return value.

Usage by example

Let’s say we want to implement a method to calculate the Fibonacci sequence, ignoring for the moment the extensions which also allow to calculate negative Fibonacci numbers.

In our use case, we would return a None for negative numbers and a Some with the calculated value. So a recursive example implementation would be:

option-implementation.ts
const fibonacci = (num: number): Option<number> => {
  const calcFibonacci = (level: number, a: number, b: number) => {
    if (level < 1) return a;
    else return calcFibonacci(level - 1, b, (a + b));
  }

  if (num < 0) return None();
  else return Some(calcFibonacci(num, 0, 1)); 
} 

The benefit of using an Option type here is that we can simply continue our calculations without worrying about the invalid case of negative numbers. See the following example in JavaScript:

option.js
const limit = 10;
const result = fibonacci(limit, 0, 1) // returns Some(55);
  .filter( (x) => x < 100 )
  .map( (x) => x + 45 )
  .getOrElse(23);

The result here would be the number 100, but the thing to notice is that throughout our (trivial) calculations, we didn’t do any error checking. If we started off with a limit of -10, the resulting None would simply be passed on, until we call getOrElse(), which would then return the passed default of 23. This is an example how the Option monad can make our lives easier since we only have to check for the successful execution at the end.

Summary

I’ve described the behavior of monads in functional programming, developed the basic behavior of the Option monad using JavaScript and showed an example of how it could be used. The great advantage of using a monad is that we don’t have to check at every step for an error; a generic check at the end of the procedure suffices, since the error is carried through. The disadvantage of using an Option is that we don’t get any information of what went wrong, only that something along the way did go wrong.

There are other functional programming paradigms such as Either and Try, which behave similarly to the Option described here, but where the None equivalent contains an error or error message, which makes analyzing errors at the end of the calculation easier. Possible use cases would be a sequence of asynchronous server calls, which might return a 4xx or 5xx status code, or complex calculations, where it is helpful to know the type of error.