Implementing the Option Monad in JavaScript
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:
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):
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:
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:
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:
- 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. - The
if
on line 5 serves to allow a programmer to create an instance of this object without usingnew
, as that instantiation occurs in theelse
clause on line 19. - 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.
- On line 22, the
getOrElse
method is defined, which returns the stored value, since this is aSome
. A method by the same name is defined on aNone
, but will return the passeddefaultValue
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 theSome
, it needs to be defined here to allow access to the privatevalue
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:
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:
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:
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:
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:
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:
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:
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.