Monads in JavaScript

comment

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:

  1. 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.
  2. 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>.
  3. 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:

  1. bind(unit(x), f) ≡ f(x)
  2. bind(m, unit) ≡ m
  3. bind(bind(m, f), g) ≡ bind(m, xbind(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).

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 TypeErrors 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:

                  1. Generating primes in ES6
                  2. Sudoku solver
                  3. Easy asynchrony with ES6
                  4. Solving riddles with Prolog and ES6 generators
                  5. Pi approximation using Monte Carlo method

                  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.

                  1. Promise.resolve(value) wraps a value and returns a promise (the unit function).
                  2. Promise.prototype.then(onFullfill: value => Promise) takes as an argument a function that transforms a value into a different promise and returns a promise (the bind 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:

                    1. Easy asynchrony with ES6
                    2. Simple AMD loader in 30 lines of code

                    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);
                            
                            var avatarUrls = user.getFriends().map(u => u.avatarUrl);
                            
                            var length = avatarUrls.filter(url => url.contains('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

                              tel
                              L
                              Very well written! (…)
                              Adam Chambers
                              Nice article explaning the often confusing concept of monads
                              ernest hughingway
                              Monads in JS using generators might just be .. useable! Exciting stuff :)
                              Thomas Parslow
                              Worth reading to the end, does some very cool things
                              Scott Robinson
                              oh god why
                              April Arcus
                              "Monads in JavaScript" by @curiositydrvorg is practical and unpretentious. My new favorite intro to the topic.
                              cancel

                              Revisions

                              1. Initial version.
                              2. Add information about Promise.prototype.catch, Either monad.
                              3. Simplify monad components section; remove Haskell type signatures.
                              4. Add related articles to List and Continuation monad sections.
                              5. Add Do notation and Chained calls sections.
                              6. Split Maybe monad constructor into Just and Nothing.