There are about five million articles on “What is the Maybe monad” - but this is not. Instead, with the help of EcmaScript 6 generator functions, we try to add some syntactic convenience to the usage of “Maybe” in the next JavaScript edition.
In order to see why all this is helpful, let’s start with some simple functions that return either a result or undefined
.
function rand() {
return Math.random() * 20
}
function f1(x) {
return x > 10 ? x : undefined
}
function f2(x, y) {
return y === 0 ? undefined : x / y
}
Chaining multiple of these functions and checking for undefined can lead to rather verbose code.
var x, y, res;
x = f1(rand());
if (x !== undefined) {
y = f1(rand());
if (y !== undefined) {
res = f2(x, y);
if (res !== undefined) {
console.log(res);
}
}
}
Of course there are multiple ways around this tour-de-france of nested if-statements including early returns, exception handling and a functional encoding using the Maybe monad or Option types (for the Scala people out there).
Today we will look at some encoding that is similar to the last mentioned solution - with the goal to support a nice syntax.
To give a preview on how the solution to the example looks like, we start rewriting the two functions f1
and f2
.
function f1(x) {
return x > 10 ? some(x) : none
}
function f2(x, y) {
return y === 0 ? none : some(x / y)
}
As it can easily be seen we just used the function some
to flag the result correct and
otherwise return the value none
(We will see how both are defined in a second).
The chaining of the method calls now can be expressed in terms of for-loops:
for (let x of f1(rand()))
for (let y of f1(rand()))
for (let res of f2(x, y))
console.log( res );
Pure awesomeness. Let’s see how this can be achieved.
function Option(value) {
this.value = value;
this.some = arguments.length == 1
}
Option.prototype.iterator = function () {
if (this.some) yield this.value
}
const none = Option.none = new Option()
const some = Option.some = v => new Option(v)
At first we create some constructor function Option
that takes an optional value to store.
Whether or not a value has been passed is saved in the property this.some
. The true ‘magic’ then happens in the prototype function iterator
which is called internally by for-loops and spread operators.
It basically works the same way as returning a singleton array
[x]
forsome(x)
and an empty array[]
fornone
.
So let’s dig into this a little bit deeper and redefine some
and none
in terms of arrays:
var some = function(v) { return [v]; };
var none = [];
// In ES6
const some = (v) => [v]
const none = Object.freeze([])
The amazing fact: The above version is of course backwards compatible to ES3. And it also works out of the box with the ES5/6 collections and libraries such as underscore.js.
> some(3).map( x => x + 2 )
[5]
> none.map( x => x / 2 )
[]
Refactoring code like
return is_true ? result : undefined
intoreturn is_true ? [result] : []
allows callers to write programs in a more functional way without loosing backwards compatibility.
Spread operator
console.log( [1, 2, ...some(3), ...none, 4] )
for comprehensions
[x + y for( x of some(3)) for(y of some(6))]
The solution is an encoding inspired by scala’s for-comprehensions. Sadly ES6 does not desugar “for-loops” into plain method calls and anonymous functions like in scala. So hacking the for syntax is rather restricted.
The easiest encoding using empty and singleton arrays for none
and some
offers the advantage that using list comprehensions the result is automatically wrapped up in the correct type.
> [x + y for( x of some(3)) for(y of none)]
[]
whereas
> for (let x of some(3))
> for (let y of none)
> x + y
undefined
yields nothing since for
is a statement and not an expression.
Nevertheless, I think both ways (the Option
function and the array encoding) offer great advantages over the common imperative idiom.