Skip to content

Instantly share code, notes, and snippets.

@tomecko
Last active February 9, 2016 19:55
Show Gist options
  • Save tomecko/ddeec6739f249cee3eaa to your computer and use it in GitHub Desktop.
Save tomecko/ddeec6739f249cee3eaa to your computer and use it in GitHub Desktop.
Monads in JavaScript

In theory there is no difference between theory and practice. In practice there is.

Yogu Berra

This article is not real theoretical explanation of monads. It is a simple practical tutorial for JavaScript developers showing how some monads can be used. It's for engineers, not scientists.


All examples are based on monet.js - a tool bag that assists Functional Programming by providing a rich set of Monads and other useful functions. In most examples I'll use arrow functions introduced in ES6 as they are much more readable for one single operation "callbacks". Also in some examples TypeScript like type definitions will be added to enhance overall readability.

Monad

Monad is a box containing some value. People say it has something to do with freaky category theory but we can ignore that fact. Our box is not just a wrapper. It has tools to bind computations to the value. It can also handle different use cases - asynochronicity, fail-fast, error accumulation, lazy evaluation, etc… but these will be covered in next articles.

Monads are defined by 3 axioms. One of the things that axioms tell us is that this box has to have some method (called bind or flatMap) that meets 3 requirements:

  • it takes one parameter - a function (aka callback)
  • it returns output of callback - a monad containing value of the same type as original monad
  • callback takes current value and returns monad containing some new value

Identity

Axioms tell us that there should be some function that creates a monad. Consider we have Identity(value) function that does the job:

var monadWith3 = Identity(3);   // number 3 wrapped in an Identity box

var monadWith5 = monadWith3.bind( value => Identity(value + 2) ); // Identity(5)

But why should I ever complicate my life so much if all I want is to add two integers?

Right. Previous example is not a real life one at all. But if you try to .bind() several operations to some value. And we may use another common for monad-like things method .map(callback). When mapping - you don't have to return a monad from callback - returned value is wrapped under the hood in a same monad:

function getPercentRatio(wrongCount, correctCount): string {
    return Identity(wrongCount)
        .map(wrongCount => wrongCount + correctCount)   // we get total count here
        .map(totalCount => correctCount / totalCount)   // ratio between 0 and 1
        .map(ratio => Math.round(100 * ratio))          // ratio between 0 and 100
        .map(percentRatio => String(ratio) + '%')       // ratio as a string like '28%'
        .get()                                          // get value from inside of the Identity
}

Here Identity with .map() works exactly like an Array:

function getPercentRatio(wrongCount, correctCount): string {
    return [wrongCount]
    
        .map(wrongCount => wrongCount + correctCount)   // we get total count here
        .map(totalCount => correctCount / totalCount)   // ratio between 0 and 1
        .map(ratio => Math.round(100 * ratio))          // ratio between 0 and 100
        .map(percentRatio => String(ratio) + '%')       // ratio as a string like '28%'
        
        .pop()                                          // get value from inside of the Identity
}

For purpose of example, calculations above are rather simple but they show idea of how to compose operations in descriptive, readable way. Consider other possibilities:

function getPercentRatio(wrongCount, correctCount): string {
    return String(Math.round(correctCount / (wrongCount + correctCount)) * 100) + '%';
}

or in more functional style:

function getPercentRatio(wrongCount, correctCount): string {
    return toPercentString(Math.round(ratio2percent(ratio(correctCount)(wrongCount))));
}

And now think about real-life, complex compositions of calculations…

Promise A/A+

Common JavaScript Promises are quite similar to monads. They are boxes containing values (or rejections). Compare this portion of code with initial Identity example:

var promiseOf3 = Q.resolve(3);   // a Promise with number 3 inside

var promiseOf5 = promiseOf3.then( value => Q.resolve(value + 2) );   // a Promise with number 5 inside

It is simple example. Actually Promise A/A+ is not as typesafe as Monads are. Method .then() is so elastic that it does job of several different methods of a monad (bind/flatMap, map, orElse, cata and few more).

But for sure there exist monadic implementations of JavaScript Promise!

Maybe (aka Option)

There are several monads in the wild. Probably the most common is Maybe (Option). It can be Some(value) or Nothing. Example that fits previous ones:

var optionOf3 = Some(3);

var optionOf5 = optionOf3.flatMap((value) => Some(value + 2));

// Name 'flatMap' is commonly used for 'bind' method. I'll stick to it for the rest of this article.

It becomes useful when we have methods/functions returning some value or null/undefined. For example getCurrentUser():

function getCurrentUser(): User { /* some implementation */ };

It can return a User entity or… null. See this:

function getId() {
    return getCurrentUser().id; // will throw error if user is null or undefined
}

We can fix this with some ugly if/else bloat:

function getId(): Maybe<string> {
    var user = getCurrentUser();
    if (user) {
        return user.id; 
    }
    return null;
}

…or we can pack our values in a Maybe box:

function getCurrentUser(): Maybe<User>;

function getId(): Maybe<string> {
    return getCurrentUser().map(user => user.id); 
}

We are safe. No Uncaught TypeError: Cannot read property 'id' of null exceptions. And we do not have to add ugly if/else complexity to code.

Maybe has few base methods:

  • flatMap which is core of any monad
  • map lets us get Maybe<string> from Maybe<User>
  • filter lets us change Some into None if a condition is not met
  • orSome - get monad value or just another value
  • orElse - get monad (if is Some) or else new passed monad
  • cata - whooo lot of magic…

Let's .filter() and .map() like an Array

If you treat a Maybe as a single element (Some) or empty (None) array - map and filter will work the same:

let maybeUser = [{id: "3asd4asd", name: "James"}];
maybeUser.map(user => user.id); // => ["3asd4asd"]
maybeUser.filter(user => user.name === "John"); // => []

let notUser = [];
notUser.map(user => user.id); // => []
notUser.filter(user => user.name === "John"); // => []

…and for Maybe:

let maybeUser = Some({id: "3asd4asd", name: "James"});
maybeUser.map(user => user.id); // => Some("3asd4asd")
maybeUser.filter(user => user.name === "John"); // => None

let notUser = None;
notUser.map(user => user.id); // => None
notUser.filter(user => user.name === "John"); // => None

Let's create a new getter (similar to getId() few lines above):

function getName(): Maybe<string> {
    return getCurrentUser().map(user => user.name); 
}

Difference from Array's map is that monad mapping should fail if callback returns no value. So if user's name is null or undefined above example will be broken. We can easily fix this with filtering:

function getName(): Maybe<string> {
    // if user.name is null or undefined map callback will never be called
    return getCurrentUser()
        .filter(user => !!user.name)
        .map(user => user.name); 
}

"But that becomes so complex…" you would say. And you are right. Basic monad method is flatMap (aka unit) and it can handle this situation.

So let's .flatMap()

function getName(userOption: Maybe<User>): Maybe<string> {
    return userOption.flatMap(user => {
        if (user.name) {
            return Maybe.Some(user.name);
        }
        return Maybe.None;
    }); 
}

…but that is still a bit ugly. And that's why my favorite JavaScript implementation (monet.js) provides additional way to create Maybe monad:

let name = 'James';
let otherName;

let maybeName = Maybe.fromNull(name); // this one is Some('James');

let maybeOtherName = Maybe.fromNull(otherName); // None

With this JavaScript oriented super power we can:

function getName(userOption: Maybe<User>): Maybe<string> {
    return userOption.flatMap(user => Maybe.fromNull(user.name)); 
}

We can easily imagine situation in which empty user name could be filled with some default value, e.g. 'Guest'. We can use .orSome() for this.

Get value .orSome() other value

Let's get user name with fallback to 'Guest':

// Some getters defined earlier:
function getCurrentUser(): Maybe<User>;
function getName(userOption: Maybe<User>): Maybe<string>;

// And the meat and potatoes:
let name = getName(getCurrentUser()).orSome('Guest');

This one in other implementations is called .getOrElse() - get value or else another passed value.

Without monads this would look like:

function getCurrentUser(): User;
function getName(user: User): string;

let user = getCurrentUSer();
let name;

if (user) {
  name = getName(user);
}

if (!name) {
  name = 'Guest';
}

Ugly, isn't it?

Now what is the difference between .orSome() and .orElse() ?

Maybe tea .orElse() maybe coffee…

We considered default value for optional name field. We may also want to fill it with optional value from any other field in our source value like 'nickname'. What can we do with filthy imperative tools:

function getName(user: User): string;
function getNick(user: User): string;

function getPrintableName(user: User) {
  let name;
  
  if (user) {
    name = getName(user);
    if (!name) {
      name = getNick(user);
    }
  }
  
  return name || 'Guest';
}

So returning to a safe ground:

function getName(userOption: Maybe<User>): Maybe<string>;
function getNick(userOption: Maybe<User>): Maybe<string>;

function getPrintableName(userOption: Maybe<User>): Maybe<string> {
  return getName(userOption).orElse(getNick(userOption)).orSome('Guest');
}

Simple, isn't it?

Catastrophism vs. catamorphism

In category theory, the concept of catamorphism (from Greek: κατά = downwards or according to; μορφή = form or shape) denotes the unique homomorphism from an initial algebra into some other algebra.

We can ignore that scientific bullshit I think. It's easy to remember that most complex part of monet.js Maybe monad is .cata() just like catastrophism. It can lead to catastrophe or cataclysm if used carelessly or without type safety on mind. It is also the best tool (along with .orSome() and .orElse()) to link our brand new functional code with old and ugly third part libraries ;)

It is like hybrid of .orSome() and .map() with 2 callbacks:

  • for case we have None - it takes none args and returns fallback value
  • for case we have Some(value) - it takes one arg (a value) and returns new value

It returns output of the callback that was called. Its signature in context of Maybe looks like this:

Maybe<T> {
    …
    cata<Z>(noneCallback: () => Z, someCallback: (val: T) => Z): Z; 
    …
}

Consider another case - we have Maybe (option of user name) and need to get greeting - different for named user and different for anonymous Guest:

function getGreeting(nameOption: Maybe<string>): string {
    return nameOption.cata(() => 'Hi Guest!', name => 'Welcome back ' + name + '!');
}

Sophisticated scientific name for simple operation.

Conclusion

This simple little monad can save you a lot of time and some of code. Identity gives you ability to write complex computations as descriptive steps. Maybe brings protection from Cannot read property of null exceptions. And what most important - this is only first step to functional programing monadic world where you can find more similar tools like Either (fail-fast handling), Validation (error accumulation), Promise (async operations), immutable List, Lazy, Continuation, Free, Read, etc…

Remember, a monad is really nothing more than a chainable computation. It is simply a functional way to sequence things. That’s it.

A Gentle Intro to Monads … Maybe? Sean Voisen

Further reading:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment