Monads in JavaScript
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 typenumber
. - the
unit
function that wraps a value of underlying type into a monad. For the Maybe monad it wraps value2
of the typenumber
into the valueMaybe(2)
of the typeMaybe<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.');
}
List monad
The list monad represents a lazily computed list of values.
The unit
function of this monad takes one value and returns a generator that yields that value. The bind
function applies the transform
function to every element and yields all elements from the result.
function* unit(value) {
yield value;
}
function* bind(list, transform) {
for (var item of list) {
yield* transform(item);
}
}
As arrays and generators are iterable the bind
function will work on them. The example below creates a lazy list of sums for every pair of elements.
- ES6 generators
- for-of loops
- Iterable arrays
var result = bind([0, 1, 2], function (element) {
return bind([0, 1, 2], function* (element2) {
yield element + element2;
});
});
for (var item of result) {
print(item);
}
These related articles show several different applications of JavaScript generators:
Continuation monad
The continuation monad is used for asynchronous tasks. Fortunately with ES6 there is no need to implement it — the Promise object is an implementation of this monad.
Promise.resolve(value)
wraps a value and returns a promise (theunit
function).Promise.prototype.then(onFullfill: value => Promise)
takes as an argument a function that transforms a value into a different promise and returns a promise (thebind
function).
// Promise.resolve(value) will serve as the Unit function
// Promise.prototype.then will serve as the Bind function
- Native promises
var result = Promise.resolve(5).then(function(value) {
return Promise.resolve(6).then(function(value2) {
return value + value2;
});
});
result.then(function(value) {
print(value);
});
Promises provide several extensions to the basic continuation monad. If then
returns a simple value (and not a promise object) it is treated as a Promise resolved to that value automatically wrapping a value inside the monad.
Second difference lies in the error propagation. Continuation monad allows passing only one value between computation steps. Promises on the other hand have two distinct values — one for the success value and one for the error (similar to the Either monad). Errors can be captured using the second callback to the then
method or using the special .catch
method.
These related articles use Promises:
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.
Comments
Revisions
- Initial version.
- Add information about
Promise.prototype.catch
, Either monad. - Simplify monad components section; remove Haskell type signatures.
- Add related articles to List and Continuation monad sections.
- Add Do notation and Chained calls sections.
- Split Maybe monad constructor into Just and Nothing.