A monad is an object that has:
- 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 typeint
, and we produce a new typeM<int>
.- A generic function unit which accepts a value of type
T
and produces a value of typeM<T>
.- A generic function bind which accepts a value of type
M<T>
and a function fromT
toM<U>
, and produces anM<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;
}
-
3Douglas Crockford gave a great (or at least interesting) talk on monads in JavaScript.Karl Bielefeldt– Karl Bielefeldt2016年07月08日 18:52:54 +00:00Commented Jul 8, 2016 at 18:52
-
The one with the macroid? Yes I may re-visit it.52d6c6af– 52d6c6af2016年07月08日 18:55:02 +00:00Commented Jul 8, 2016 at 18:55
3 Answers 3
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. :)
-
I think you meant to use
bind: function (f) { return IdentityMonad.unit(f(val)); }
, otherwise you can't chain the bindings.zzzzBov– zzzzBov2016年07月08日 19:23:05 +00:00Commented Jul 8, 2016 at 19:23 -
2my 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– zzzzBov2016年07月08日 19:39:08 +00:00Commented Jul 8, 2016 at 19:39 -
1@zzzzBov - Yes, you're quite right... will update answer. :)Jules– Jules2016年07月08日 21:25:50 +00:00Commented Jul 8, 2016 at 21:25
-
1No, he's quite wrong. bind() should return a monadic value. Having
function(x) { return x + 1; }
is totally correct. His first comment about bind returningunit(f(val))
is correct.DeadMG– DeadMG2016年07月08日 21:39:58 +00:00Commented Jul 8, 2016 at 21:39 -
function(x) { return x + 1; }
isn't a valid argument tobind
, but it is a valid argument tofmap
Caleth– Caleth2019年07月12日 10:41:00 +00:00Commented Jul 12, 2019 at 10:41
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:
Array<T>
generic type which is instantiated to a specific array type likeArray<number>
.flatMap()
has the typeArray<T>.flatMap(T=> Array<U>): Array<U>
Array.of
has the typeArray.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.
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),
};
-
1I would consider caching/sharing the
none
case.Alexander– Alexander2020年10月29日 19:32:10 +00:00Commented Oct 29, 2020 at 19:32