0

A monad is an object that has:

  1. A transformation that takes a type and produces a new type. In C# we call such a transformation a "generic type". We have a generic type M<T>, we have a type int, and we produce a new type M<int>.
  2. A generic function unit which accepts a value of type T and produces a value of type M<T>.
  3. A generic function bind which accepts a value of type M<T> and a function from T to M<U>, and produces an M<U>.

But what would this look like in a weakly, dynamically typed language like JavaScript?

First attempt:

// 1 is not possible because of the typing system?

function monadify(m) {  // `m` is a function
  if(typeof m !== 'function') {
    throw new TypeError('m must be a function');
  }

  // Returns a function that will apply m to t.
  // "Wraps the value t".
  m.unit = function(t) {  // 2
    return function() { 
      return m(t); 
    };
  };

  // Returns a function that will apply `m` to `t` and then apply `fn` to the result.
  // Binds `fn` to `m`.
  m.bind = function(m, fn) { 
    return function(t) { // 3
      return fn(m(t));
    };
  };

  return m;
}
2
  • 3
    Douglas Crockford gave a great (or at least interesting) talk on monads in JavaScript. Jul 8, 2016 at 18:52
  • The one with the macroid? Yes I may re-visit it.
    – 52d6c6af
    Jul 8, 2016 at 18:55

3 Answers 3

2

You're right that the "type constructor" part of the definition (which Eric described as a type transformation) isn't really relevant in a dynamically typed language. So all you really need are the two functions, unit and bind. In Javascript, the identity monad might look like this:

IdentityMonad = {
    unit: function (val) { 
        return {
            bind: function (f) { return f(val); }
        };
    }
};

Using this would look something like:

IdentityMonad.unit(4)
             .bind(function (x) { return IdentityMonad.unit(x+1); })
             .bind(function (x) { window.alert(x); });

Building actually useful monads is left as an exercise to the reader. :)

5
  • I think you meant to use bind: function (f) { return IdentityMonad.unit(f(val)); }, otherwise you can't chain the bindings.
    – zzzzBov
    Jul 8, 2016 at 19:23
  • 2
    my prior comment may be incorrect as well, I believe .bind(function (x) { return x + 1; } is where the actual issue lies, as the function itself should be returning the monad as .bind(function (x) { return IdentityMonad.unit(x + 1); }) but my knowledge of monads is relatively limited at this point so I could be completely wrong.
    – zzzzBov
    Jul 8, 2016 at 19:39
  • 1
    @zzzzBov - Yes, you're quite right... will update answer. :)
    – Jules
    Jul 8, 2016 at 21:25
  • 1
    No, he's quite wrong. bind() should return a monadic value. Having function(x) { return x + 1; } is totally correct. His first comment about bind returning unit(f(val)) is correct.
    – DeadMG
    Jul 8, 2016 at 21:39
  • function(x) { return x + 1; } isn't a valid argument to bind, but it is a valid argument to fmap
    – Caleth
    Jul 12, 2019 at 10:41
1

Arrays in JavaScript are monads according to (2) and (3): flatMap() is bind and Array.of() is return.

Requirement (1) does not really apply to a language without a type notation, but if we use TypeScript we get:

  1. Array<T> generic type which is instantiated to a specific array type like Array<number>.
  2. flatMap() has the type Array<T>.flatMap(T=> Array<U>): Array<U>
  3. Array.of has the type Array.Of(T): Array<T>

Which satisfies the definition.

Whether it is useful to think of Array as a monad is a different question. In my opinion, monads are not a very useful abstraction in a language like JavaScript.

1

Here is a partially implemented maybe monad for anyone revisiting.

class Maybe {
    isSome;
    val;
    constructor(isSome, val) {
        this.isSome = isSome;
        this.val = val;
    }
    orJust(t) {
        return this.isSome
            ? this.val
            : t;
    }
    map(fn) {
        return new Maybe(this.isSome,
            this.isSome
                ? fn(this.val)
                : null);
    }
    chain(fn) {
        return this.isSome
            ? fn(this.val)
            : new Maybe(false, null);
    }
}

export default {
    some: (val) => new Maybe(true, val),
    none: new Maybe(false, null),
    fromUndef: (val) => !!val && (val) !== NaN
        ? new Maybe(true, val)
        : new Maybe(false, null),
};
1
  • 1
    I would consider caching/sharing the none case.
    – Alexander
    Oct 29, 2020 at 19:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.