Tuesday, June 4, 2013

Monads in plain JavaScript

So you want to know what these Monads thing is? Douglas Crockford in an interview said that if you understand them you lose the ability to explain them, and then went on to do a Monads & Gonads talk. In the talk he said you don't have to understand category theory or Haskell and I happen to agree, but then he jumps in to some generic monad constructor that may be confusing at first. So what's a monad in plain english?

Simply put it's a wrapper around a value. There are also some laws that it should conform to, but more on that later.

As with any wrapper we need two methods, one to get and one to set the value. These are called "return" and "bind". Let's construct the "Maybe" monad - the easiest monad to get your head around:

var Maybe = function(value) {
  this.value = value;
};

So we have a constructor, monads are something called a "functor" which basically is a "functional object" that allows you to move values between sets. In this case the maybe monad will allow us to use "undefined" in places which expect an actual value and we would see an error otherwise.

So let's look at the return function:

Maybe.prototype.ret = function() {
  return this.value;
};

Nice and easy, it gives us a method to get the value of the monad. Now comes the fun part, bind:

Maybe.prototype.bind = function(fn) {
  if (this.value != null)
    return fn(this.value);
  return this.value;
};

So the bind function runs a given function with the value of the monad. In the case of the maybe monad it just skips running the function if the value doesn't exist - and that's it!

Now we should talk about "lift". you'll see that the bind will return whatever the function returns for the value. Well we want a monad as a return so you should be passing in functions that return a monad - but who wants to do that? instead we'll just create a "lift" function that can take in a function that returns a normal value and changes it to a monad. pretty easy, it'd look something like this:

Maybe.lift = function(fn) {
  return function(val) {
    return new Maybe(fn(val));
  };
};

so now we can do things like have an addOne function:

var addOne = function(val) {
  return val + 1;
};

lift it:

var maybeAddOne = Maybe.lift(addOne);

then you can use it with bind! But what if we have a function that takes two monads? say we want to add two together?

Maybe.lift2 = function(fn) {
  return function(M1, M2) {
    return new Maybe(M1.bind(function(val1) {
      return M2.bind(function(val2) {
        return fn(val1, val2);
      });
    }));
  };
};

This one's a bit more complicated, but basically it just uses closures to get the values from the two monads before running it through the function, and because it's a maybe monad it will just pass back undefined  so we can safely use undefined values without errors. You can try it out like this:

var add = function(a, b) {return a + b;};
m1 = new Maybe(1);
m2 = new Maybe(2);
m3 = new Maybe(undefined);

var liftM2Add = Maybe.lift2(add);

liftM2Add(m1, m2).ret(); //3
liftM2Add(m3, m2).ret(); //undefined
liftM2Add(m1, m3).ret(); //undefined

And that's it. So to recap a monad is just a container. You can pass in a function to operate on it and get a returned value, or ask for it's value. You've probably used monads in the past without knowing (like promises, you just send it a function to run on it's value and it returns back itself - so it's a lifted bind) and perhaps even created some.



13 comments:

  1. In snippet 4 you open two curly brackets but close only one. In snippet 7, line 7 there's a closing parentheses missing. Snippet 8 line 8 doesn't work. "liftM2Add(m1,m2)" results in an error. "liftM2Add.value(m1, m2)" works.

    ReplyDelete
    Replies
    1. Thankyou - I guess I should test my code before putting it up online. I needed to put the "new Maybe" down a line, it should work as intended now

      Delete
    2. The errors are solved but the "monads" still don't work as expected. "m1.value = 5;" shouldn't be possible with a monad.

      Delete
    3. such is JavaScript. The aim of this is not to be perfect but provide enough for a basic understanding. I'm aiming for usefulness rather than purity in this definition.

      Delete
    4. Can be accomplished by having value being a protected property. All Methods must then use
      privileged method Monad#ret instead of accessing Monad#value directly

      Delete
  2. Unfortunately, this is not a correct implementation of a monad. Think about the signatures of the two functions (in Haskell):

    > :t (>>=)
    (>>=) :: Monad m => m a -> (a -> m b) -> m b
    > :t (return)
    (return) :: Monad m => a -> m a

    I'm not sure if you understand Haskell code, but the main point is that both `bind` and `ret` should return a Monad again. In your case, both of them return the type of `this.value`, except when you call `fn`, in which case they return whatever `fn` returns, which *must* be a monad according to the types.

    Here's my take on it: https://gist.github.com/igstan/5735974

    The null-checking logic resides in two places:

    1. the Option function
    2. the dynamic dispatch mechanism, which will chose the approapriate
    implementation of `bind` or `unit` at runtime.


    Also, Monad "inherits" (or at least, it should) from Functor. A functor is just a data type which can be mapped over, i.e. it provides a `map` method. That's all.

    > :t fmap
    fmap :: Functor f => (a -> b) -> f a -> f b

    Hope it helps! And maybe you should try to implement the List monad now that you know the type constraints.

    ReplyDelete
    Replies
    1. This comment has been removed by a blog administrator.

      Delete
  3. I am bit confused, was the intention of your post to explain what a Monad is or to show how to do it in JS? I have not used Monads, so I got lost when you started talking about lift. The paragraphs about lift could be clearer and provide more context. Thanks for the post anyway.

    ReplyDelete
  4. A CoffeeScript version of your code:
    https://gist.github.com/jiyinyiyong/5746958

    ReplyDelete
  5. At the risk of making myself look stupid, here's one of my recent attempts at something similar to the Maybe monad. I've written the code in CoffeeScript for ease of writing. For this exercise I focussed on a use case - retrieving an object's property value that may, or may not, exist.

    Anyway, here's the link to the repo: https://github.com/KarlPurk/maybe

    I would love to hear feedback, even if it's simply stating how this implementation is not a Maybe Monad :)

    ReplyDelete
    Replies
    1. Nice try, but no monad :-) You may want to check out the gist proposed in the second comment.

      Delete
  6. "A monad is just a container."

    "Monads are something called a "functor" which basically is a "functional object"".

    "...a lifted bind". (a WHAT?!?!?)

    I am afraid your grasp on this subject cannot be sound, as long as you come out with such flawed assertions.

    Unsurprisingly, one sees at a glance that your implementations of return and bind are simply, plainly ...wrong.

    I say that the code you present in this page won't pass any of the monad laws proofs and that the only value that a reader can take away from this page is what derives from understanding its fallacies.

    Allegedly you've spent some time on Crockfords code. I suggest you get back there and give it a good study again. To convince yourself of the value of my suggestion, you may want to start by comparing the results of basic Maybe computations produced by his code and yours. Cheers.

    ReplyDelete
  7. Regardless of some of the criticism regarding the details of your examples in the comments here, you communicated the general concepts quite clearly and I for one greatly appreciate this well-thought post!

    ReplyDelete