Note: while the code in this question deals with functional programming/monads, etc., I'm not asking about functional programming (nor do I think this question should have tags related to functional programming, etc.). Instead, I'm asking about the use of JavaScript's prototype.
Code Source
I'm watching Douglas Crockford's video entitled "Monads and Gonads" (on YouTube either here or here). He includes a demo implementation of a monad in JavaScript, shown below.
The monad
Object and its Prototype
In his code, he creates a truly-empty object using Object.create(null)
and uses this as the prototype for his eventual monad
object. He attaches the bind
method to the monad object itself, but any custom functions that are later attached to the monad using lift
are attached not to the monad object itself but to its prototype.
Prototype Needed?
It seemed to me that the use of a prototype was unnecessary complexity. Why couldn't those custom functions simply be attached directly to the monad object itself? Then, it seemed to me, the prototype wouldn't be needed, and we could simplify the code.
Puzzling Results When Prototype Removed
I tried implementing this simplification and got puzzling results. The non-prototype-using code sometimes still worked, i.e. it could still use the monad-wrapped value (the string "Hello world.") when the custom function was invoked with no extra parameters (monad2.log()
). However, when the custom function was invoked using extra parameters (monad2.log("foo", "bar")
), the code was now unable to find value
even though it could still use those extra parameters.
Update on Puzzling Results: In part because of the answer from @amon, I have realized that the puzzling results do not appear because I change the number of parameters, but rather because I simply repeat a call to a lift
ed method on a monad (whether or not the number of parameters has changed). Thus, running monad2.log()
twice in a row will yield the correct value the first time but will be undefined the second time.
Questions
So, why is the prototype needed in this code? Or, alternatively, how does eliminating the prototype cause value
to be accessible some times but not other times?
Demo Code Descriptions
The two versions of the code are shown below. The prototype-using code (MONAD1
) is identical to the code Crockford uses in his video except that the custom function attached is console.log
instead of alert
so that I could play with this in node rather than in a browser. The non-prototype-using code (MONAD2
) makes the changes indicated in the comments. The output is shown in the comments.
The Prototype-Using Code
function MONAD1() {
var prototype = Object.create(null); // later removed
function unit (value) {
var monad = Object.create(prototype); // later moved
monad.bind = function (func, ...args) {
return func(value, ...args);
}
return monad;
}
unit.lift = function (name, func) {
prototype[name] = function (...args) { // later changed
return unit(this.bind(func, ...args));
};
return unit;
};
return unit;
}
var ajax1 = MONAD1()
.lift('log', console.log);
var monad1 = ajax1("Hello world.");
monad1.log(); // --> "Hello world."
monad1.log("foo", "bar"); // --> "Hello world. foo bar"
The Non-Prototype-Using Code
function MONAD2() {
// var prototype = Object.create(null); // removed
var monad = Object.create(null); // new
function unit (value) {
// var monad = Object.create(prototype); // removed
monad.bind = function (func, ...args) {
return func(value, ...args);
}
return monad;
}
unit.lift = function (name, func) {
monad[name] = function (...args) { // changed
return unit(this.bind(func, ...args));
};
return unit;
};
return unit;
}
var ajax2 = MONAD2()
.lift('log', console.log);
var monad2 = ajax2("Hello world.");
monad2.log(); // --> "Hello world." i.e. still works
monad2.log("foo", "bar"); // --> "undefined foo bar" i.e. ???
JSBin
I've played with this code in node, but you can see the results in this jsbin. Console.log
doesn't seem to work exactly the same in jsbin as it does in node in the terminal, but it still shows the same puzzling aspects of the results. (The jsbin doesn't seem to work if you just click on 'Run' in the console pane. Rather, you have to activate the output pane by clicking on the 'Output' tab, and then click on 'Run with js' in the 'Output' pane to see the results in the 'Console' pane.)
You have to make a clear distinction between a particular type of monad, and a monad instance that actually contains a value. Your second example is mixing the two up in a manner that I'll discuss in a moment.
First, the MONAD
function constructs a new monad type. The concept “monad” is not a type in itself. Instead, the function creates a type that has monad-like behaviour:
unit
operation wraps a value inside a monad. It is a kind of constructor: monadInstance = MonadType(x)
. In Haskell: unit :: Monad m => a -> m a
.bind
operation applies a function to the value(s) within a monad instance. That function must return a monad of the same type. The bind operation then returns the new monad: anotherMonadInstance = monadInstance.bind(f)
. In Haskell: bind :: Monad m => m a -> (a -> m b) -> m b
.You can think of the MonadType
and the unit()
operation as being more or less the same thing. The reason why we create a separate prototype is that we don't want to inherit random baggage from the “function” type. Also, by hiding it within the monad-type constructor, we protect it from unchecked access – only lift
can add new methods.
The lift
operation is not essential, but mightily convenient. It allows functions that works on plain values (not monad instances), to be applied to monad instances instead. Usually, it would return a new function that operates on the monad level: functionThatReturnsAMonadInstance = lift(ordinaryFunction)
. In Haskell: lift :: Monad m => (a -> b) -> (a -> m b)
. But which kind of monad is lift
supposed to return? To keep this context, each lifted function is bound to a particular MonadType
. Note: not just a particular monadInstance
! Once a function is lifted, we can apply it to all monads of the same type.
I'll now rewrite the code to make these terms more clear:
function CREATE_NEW_MONAD_TYPE() {
var MonadType = Object.create(null);
function unit (value) {
var monadInstance = Object.create(MonadType);
monadInstance.bind = function (func, ...args) {
return func(value, ...args);
}
return monadInstance;
}
unit.lift = function (name, func) {
MonadType[name] = function (...args) {
return unit(this.bind(func, ...args));
};
return unit;
};
return unit;
}
var MyMonadType = CREATE_NEW_MONAD_TYPE()
MyMonadType.lift('log', console.log); // adds MyMonadType(…).log(…)
var monadInstance = MyMonadType("Hello world.");
monadInstance.log(); // --> "Hello world."
monadInstance.log("foo", "bar"); // --> "Hello world. foo bar"
What happens in your code is that you get rid of the monadInstance
. Instead, you add a bind
operation to the MonadType
! This bind
operation happens to refer to the last value that was wrapped with unit()
.
Now note that the return value of lifted functions is wrapped as a monad with unit
.
monadInstance
(which is actually MonadType
), then MonadType.bind()
refers to the "Hello World"
value.log()
function. It receives the value within the monad, which is the last value that was wrapped with unit()
, which is "Hello World"
. The return value of the lifted function (console.log
) is wrapped with unit()
. This return value is undefined
. You then replace the bind
function with a new bind
that refers to the undefined
value.log()
function. It receives the value within the monad, which is the last value that was wrapped with unit()
, which is undefined
. The observed output ensues.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With