Using ES6 generators to encode Option Types

Posted by Jonathan Immanuel Brachthäuser on October 29, 2013 · 8 mins read

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.

Motivation

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.

The Solution

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.

The Implementation

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] for some(x) and an empty array [] for none.

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 )
[]

Lessons Learned

Refactoring code like return is_true ? result : undefined into return is_true ? [result] : [] allows callers to write programs in a more functional way without loosing backwards compatibility.

Other Examples

Spread operator

console.log( [1, 2, ...some(3), ...none, 4] )

for comprehensions

[x + y for( x of some(3)) for(y of some(6))]

Summary

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.