Monads in JavaScript

7 min read

Monad is a design pattern used to describe computations as a series of steps. They are extensively used in pure functional programming languages to manage side effects but can also be used in multiparadigm languages to control complexity.

Monads wrap types giving them additional behavior like the automatic propagation of empty value (Maybe monad) or simplifying asynchronous code (Continuation monad).

To be considered a monad the structure has to provide three components:

type constructor — a feature that creates a monadic type for the underlying type. For example it defines the type Maybe<number> for the underlying type number . the unit function that wraps a value of underlying type into a monad. For the Maybe monad it wraps value 2 of the type number into the value Maybe(2) of the type Maybe<number> . the bind function that chains the operations on a monadic values.

The following TypeScript code shows the signatures of those generic functions. Assume that the M indicates a monadic type.

interface M<T> { } function unit<T>(value: T): M<T> { // ... } function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> { // ... }

This bind function is not the same as the Function.prototype.bind function. The latter is a native ES5 function. It is used to create a partially applied functions or functions with bound this value.

In the object oriented languages like JavaScript the unit function can be represented as a constructor and the bind function as an instance method.

interface MStatic<T> { // constructor that wraps value new(value: T): M<T>; } interface M<T> { // bind as an instance method bind<U>(transform: (value: T) => M<U>): M<U>; }

There are also three monadic laws to obey:

bind(unit(x), f) ≡ f(x) bind(m, unit) ≡ m bind(bind(m, f), g) ≡ bind(m, x ⇒ bind(f(x), g))

The first two laws say that the unit is a neutral element. The third one says that the bind should be associative — the order of binding does not matter. This is the same property that the addition have: (8 + 4) + 2 is the same as 8 + (4 + 2) .

The examples below require the arrow function syntax support. Firefox (version 31) supports the arrow functions natively while Chrome does not support them (version 36).

function print(msg) { var special = false; if (typeof msg === 'undefined') { msg = 'undefined'; special = true; } if (msg === null) { msg = 'null'; special = true; } var li = document.createElement('LI'); li.textContent = msg.toString(); li.className = special ? 'primitive' : ''; output.appendChild(li); } function clear() { output.innerHTML = ''; } clear();

Identity monad The identity monad is the simplest monad. It just wraps a value. The Identity constructor will serve as the unit function. function Identity(value) { this.value = value; } Identity.prototype.bind = function(transform) { return transform(this.value); }; Identity.prototype.toString = function() { return 'Identity(' + this.value + ')'; }; The example below computes addition using the Identity monad. Arrow functions var result = new Identity(5).bind(value => new Identity(6).bind(value2 => new Identity(value + value2))); print(result);

Maybe monad The maybe monad is similar to the identity monad but besides storing a value it can also represent the absence of any value. Just constructor is used to wrap the value: function Just(value) { this.value = value; } Just.prototype.bind = function(transform) { return transform(this.value); }; Just.prototype.toString = function() { return 'Just(' + this.value + ')'; }; And Nothing represents an empty value. var Nothing = { bind: function() { return this; }, toString: function() { return 'Nothing'; } }; The basic usage is similar to the identity monad: Arrow functions var result = new Just(5).bind(value => new Just(6).bind(value2 => new Just(value + value2))); print(result); The main difference from the identity monad is the empty value propagation. When one of the steps returns a Nothing then all subsequent computations are skipped and Nothing is returned. The alert function below is not executed because the previous step returns the empty value. Arrow functions var result = new Just(5).bind(value => Nothing.bind(value2 => new Just(value + alert(value2)))); print(result); This behavior is similar to the special value NaN (not-a-number) in numeric expressions. When one of the intermediate results are NaN then the NaN value propagates through the computations. var result = 5 + 6 * NaN; print(result); Maybe can be used to protect against errors caused by the null value. The example code below returns an avatar for a logged in user. function getUser() { return { getAvatar: function() { return null; // no avatar } }; } Not checking for the empty values in a long method call chain can cause TypeError s when one of the returned objects is null . try { var url = getUser().getAvatar().url; print(url); // this never happens } catch (e) { print('Error: ' + e); } The alternative is using null checks but that can quickly make the code much more verbose. The code is correct but the one line turns into several. var url; var user = getUser(); if (user !== null) { var avatar = user.getAvatar(); if (avatar !== null) { url = avatar.url; } } print(url); Maybe provides another way. It stops the computations when an empty value is encountered. Arrow functions function getUser() { return new Just({ getAvatar: function() { return Nothing; // no avatar } }); } var url = getUser() .bind(user => user.getAvatar()) .bind(avatar => avatar.url); if (url instanceof Just) { print('URL has value: ' + url.value); } else { print('URL is empty.'); }

Do notation Haskell provides special syntactic sugar for working with monadic code — the do notation. A block starting with the do keyword is translated into calls to the bind function. ES6 generators can be used to mimic the do notation in JavaScript producing a simple, synchronously looking code. Previous example using the Maybe monad using direct calls to bind: Arrow functions

ES6 generators var result = new Just(5).bind(value => new Just(6).bind(value2 => new Just(value + value2))); print(result); The same code expressed as a generator. Each call to yield unwraps the value from monad: var result = doM(function*() { var value = yield new Just(5); var value2 = yield new Just(6); return new Just(value + value2); }()); print(result); This small routine wraps the generator and subsequently calls bind on values that are passed to yield : function doM(gen) { function step(value) { var result = gen.next(value); if (result.done) { return result.value; } return result.value.bind(step); } return step(); } The same routine can be used with other monads like the Continuation monad. Promise.prototype.bind = Promise.prototype.then; var result = doM(function*() { var value = yield Promise.resolve(5); var value2 = yield Promise.resolve(11); return value + value2; }()); result.then(print); The then function is aliased to bind to be consistent with other monads. For more details on using generators with promises see Easy asynchrony with ES6.

Chained calls Another way of simplifying the monadic code is by using Proxies. The function below wraps a monad instance and returns a proxy object that automatically forwards each unknown property access and function invocation to the value inside the monad. Arrow functions

ES6 Proxies function wrap(target, unit) { target = unit(target); function fix(object, property) { var value = object[property]; if (typeof value === 'function') { return value.bind(object); } return value; } function continueWith(transform) { return wrap(target.bind(transform), unit); } return new Proxy(function() {}, { get: function(_, property) { if (property in target) { return fix(target, property); } return continueWith(value => fix(value, property)); }, apply: function(_, thisArg, args) { return continueWith(value => value.apply(thisArg, args)); } }); } This wrapper can be used to provide safe access to potentially empty object references the same way to the existential operator ( ?. ). function getUser() { return new Just({ getAvatar: function() { return Nothing; // no avatar } }); } var unit = value => { // if value is a Maybe monad return it if (value === Nothing || value instanceof Just) { return value; } // otherwise wrap it in Just return new Just(value); } var user = wrap(getUser(), unit); print(user.getAvatar().url); Avatar is not present but the call to url still succeeds and produces an empty value. The same wrapper can be used to lift regular function calls into the continuation monad. The code below returns the number of friends that have a certain avatar. The example looks like it is operating on data in memory while in reality it is operating on asynchronous data. Promise.prototype.bind = Promise.prototype.then; function User(avatarUrl) { this.avatarUrl = avatarUrl; this.getFriends = function() { return Promise.resolve([ new User('url1'), new User('url2'), new User('url11'), ]); }; } var user = wrap(new User('url'), Promise.resolve.bind(Promise)); var avatarUrls = user.getFriends().map(u => u.avatarUrl); var length = avatarUrls.filter(url => url.includes('1')).length; length.then(print); Note that because all property accesses and function calls have been lifted into the monad they always produce Promises and never simple values. For more details on ES6 Proxies see Array slices.