As I learn about monads after reading dozens of tutorials I'm trying to implement the pattern in JavaScript. I'm using LiveScript and Prelude to translate some Haskell monad examples.
So the monad I'm trying to implement as an exercise is the List monad. I wrote the following, in LiveScript:
List = do ->
# unit :: a -> ma
unit = (a) -> [a]
# bind :: ma -> (a -> mb) -> mb
bind = (ma, f) --> concat (map f) ma
# lift :: (a -> b) -> ma -> mb
lift = (f, ma) --> bind ma, (a) -> unit f a
{unit, bind, lift}
add1 = (x) -> x+1
let {unit, bind} = List
x <- bind [1]
y <- bind [2]
z <- bind [3]
unit add1 x+y+z #=> [7]
(List.lift add1) [1 2 3] #=> [2 3 4]
The LiveScript syntax to nest functions at the same level of indentation is quite handy, but it translates to a JavaScript callback hell obviously:
List = function(){
var unit, bind, lift;
unit = function(a){
return [a];
};
bind = curry$(function(ma, f){
return concat(map(f)(ma));
});
lift = curry$(function(f, ma){
return bind(ma, function(a){
return unit(f(a));
});
});
return { unit: unit, bind: bind, lift: lift };
}();
add1 = function(x){
return x + 1;
};
(function(arg$){
var unit, bind;
unit = arg$.unit, bind = arg$.bind;
bind([1], function(x){
return bind([2], function(y){
return bind([3], function(z){
return unit(add1(x + y + z));
});
});
});
}.call(this, List));
List.lift(add1)([1, 2, 3]);
What I want is to implement the pattern on the receiver to be able to use it like so:
List([1]).bind([2]).bind([3]).do(function(x,y,z){ x+y+z });
After watching this video where Crockford explains monads in JavaScript (code), I understand that the MONAD
he presents is just an object with methods that could be also be implemented with prototypes. The unit
method is the constructor and bind
is an instance method that runs a function on the value with the given arguments. Then lift
adds a new method to the prototype that runs the given function on the monadic value.
But, is it an actual monad or a monadic pattern like jQuery? I have doubts about this particular explanation of monads because there is no computation sequence, the bind
method runs the function right away and returns a new instance of the "monad", it is not composition, as in the monad I implemented in LiveScript which was based on the Haskell examples.
So my question is:
Is my implementation of monad correct?
Yes, your unit
and bind
functions do what is expected from the List monad.
However, the second line, List([1]).bind([2]).bind([3]).do(function(x,y,z){ x+y+z });
does not look like a monad at all.
Those LiveScript backcalls, just as Haskell's do-notation, is just syntactical sugar for the necessary lift
callback hell. You still will need to write:
List([1]).bind((x)->List([2]).bind((y)->List([3]).bind((z)->List.unit(x+y+z))))
If expressed in terms of bind
it will always be a "mess". To make it better (and more performant), you'd use a list comprehension:
concat(for x in [1] for y in [2] for z in [3]: x+y+z)
Another idea: due to JavaScript's lax typing it should be possible to implement a true generic (recursive?) liftN
function with variadic arguments if you want:
function lift(n, fn) {
var argsPos = 2;
if (typeof n != "number") {
fn = n;
n = fn.length;
argsPos--;
}
var args = [].slice.call(arguments, argsPos);
if (n < args.length) // curry
return function(){
return lift.apply(null, [n, fn].concat(args, [].slice.call(arguments)));
}
return (function bindr(bound, args)
if (!args.length)
return unit(fn.apply(null, bound));
return bind(args[0], function(a) {
return bindr([x].concat(bound), args.slice(1));
});
})([], args);
}
If you want to use a more object-orientated pattern, then you could map the single combinations to argument tuples which you can apply in the end:
List([1]).nest(List([2])).nest(List([3])).do(function(x,y,z){ x+y+z })
// where
Monad.prototype.map = function(fn) {
var unit = this.constructor; // ???
return this.bind(function(x)
return unit(fn(x));
});
};
Monad.prototype.nest = function(m2) {
return this.map(function(x) {
return m2.map(function(y)
return [x, y]; // tuple
});
});
});
Monad.prototype.do = function(fn, n) {
function flatten(n, t) {
return n<=1 ? [t] : flatten(n-1, t[0]).concat([t[1]]);
}
return this.map(function(ts) {
return fn.apply(null, flatten(n || fn.length, ts));
});
};
Is Crockford's implementation of monad correct?
Maybe. He does implement the idendity monad, but the code looks as if he wants to extend it to other monads by overwriting the bind
method, which might not work easily for all monads.