Skip to content

Instantly share code, notes, and snippets.

@zipcode
Last active April 26, 2017 19:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zipcode/ca2f7d46184dc22b95bc31d0f3d40e15 to your computer and use it in GitHub Desktop.
Save zipcode/ca2f7d46184dc22b95bc31d0f3d40e15 to your computer and use it in GitHub Desktop.
Lies we're told about monads

What the hell are monads?

Monads are a useful tool in functional programming. Unfortunately, they're a pain in the ass to understand. I don't think they're actually that complicated, it's just that nobody seems to explain them well.

A monad is a collection which implements a flatMap method. That's it.

flatMap is a function made out of flatten and map: Collection.prototype.flatMap = function(f) { return this.map(f).flatten(); }

Which is to say, a monad is a collection with a map method and a flatten method.

Now, if you read the haskell wiki you'll find a lot of stuff about bind and return. bind is another name for flatMap. return (or pure) is another name for the constructor.

There's a couple of extra rules, known as the monad laws. These basically state that flatMap behaves in the way you'd expect: if you pass flatMap the constructor it should return the same value as you started with because you're just wrapping and then unwrapping the value. Things like that.

You should already be familiar with many data structures which implement map and flatten, such as lists and arrays. A less well-known data structure which implements the monad interface is known variously as Option or Maybe. This data structure is a zero-or-one element list: either it contains a value or it doesn't. These are usually represented as Some(value) or None when the data type is called Option, and Just(value) and Nothing when it's called Maybe. It's the same thing.

The map method looks like this: Some(value).map(f) returns Some(f(value)) whereas None.map(f) returns None again. Meanwhile, flatten turns Some(Some(value)) into Some(value) while turning None and Some(None) into None. So, roughly speaking, flatMap means "If this contains a value, use this function which will return another Option. Unwrap it sensibly."

You would use flatMap in this way to handle potential failure. For example, suppose you're doing some mathematical operations and might divide by zero.

// Javascript
// Ignore the setup if you don't understand it.

// Define flatMap in terms of map and flatten
var Monad = { 
  flatMap: function(f) { 
    return this.map(f).flatten();
  }
}

// None doesn't need values so it's a bare object
var None = {
  map: function() { return None; },
  flatten: function() { return None; },
  name: "None"
}
Object.setPrototypeOf(None, Monad);

// Some needs values, so it's a constructor
function Some(x) {
  this.value = x;
}
Some.prototype = {
  map: function(f) { 
    return (new Some(f(this.value))); 
  },
  flatten: function() {
    if (this.value.constructor == this.constructor) {
      return this.value;
    } else {
      return None
    }
  }
}
Object.setPrototypeOf(Some.prototype, Monad);


// The example proper
function reciprocal(x) {
  if (x == 0) {
    return None;
  } else {
    return new Some(1 / x);
  }
}

var x = new Some(10);
var y = new Some(0);

// Let's try it out
x.flatMap(reciprocal); // Some {value: 0.1}
y.flatMap(reciprocal); // None
x.flatMap(reciprocal).flatMap(reciprocal); // Some {value: 10}
y.flatMap(reciprocal).flatMap(reciprocal); // None
reciprocal(10).map(function (x) { return x * 2 }); // Some {value: 0.2}

function double(x) { return x * 2; }
function half(x) { return reciprocal(x).map(double).flatMap(reciprocal) }
half(9) // Some {value: 4.5}

In this way, we can gracefully handle the possibility of division by zero. It's another way of returning undefined or null except we never need to check if there's a value to use because flatMap handles that magic for us.

While collections implement the monad interface, they're not the only things that can. Functions, for example, can do the same thing - so long as they're functions with a legible definition of flatten. You could view an array as a function: when you say arr[9] is it so different from arr(9)?

The most common example you've likely encountered is Promises. Promises allow you to handle asyncronous execution by specifying a function to run when the data gets back: fetchData().then(extractBody).then(function (x) { return int(x) * 9; }) and so on. .then() is a hybrid of map and flatMap: if you pass it a function that returns a Promise, it extracts the value for you.

The real deep magic of monads is where they start to be used for function composition. But the basic idea, which is never expressed clearly and makes it seem drastically complicated from the very start, is really quite simple: monad is an interface. "A monad" is a thing which implements that interface. That interface is that you have map and flatten methods that behave correctly in relation to each other. That's all.

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