Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a monad prototype required for Douglas Crockford's monad demo code?

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 lifted 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.)

like image 202
Andrew Willems Avatar asked Oct 29 '22 17:10

Andrew Willems


1 Answers

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:

  • The 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.
  • The 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.

  • When you construct the monadInstance (which is actually MonadType), then MonadType.bind() refers to the "Hello World" value.
  • You invoke the lifted 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.
  • You invoke the lifted 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.
like image 90
amon Avatar answered Nov 15 '22 04:11

amon