Why is the prototype monad required for the Douglas Crockford monad demo code?

Note: while the code in this question is about functional programming / monads, etc., I am not asking about functional programming (and I do not think this question should have tags related to functional programming, etc. ) Instead, I ask about using a JavaScript prototype .

Source code

I watch a Douglas Crockford video called Monads and Gonads (on YouTube or here or here ). It includes a demo implementation of the monad in JavaScript, shown below.

The monad object and its prototype

In his code, he creates a really empty object using Object.create(null) and uses it as a prototype for his possible monad object. It attaches the bind method to the monad object itself, but any user-defined functions that are later tied to the monad using lift are not tied to the monad object itself, but to its prototype.

The necessary prototype?

It seemed to me that using a prototype was unnecessary complexity. Why can't these custom functions just bind directly to the monad object itself? Then it seemed to me that a prototype would not be needed, and we could simplify the code.

Perplexed results when removing a prototype

I tried to implement this simplification and got puzzling results. Sometimes code that does not support the prototype still worked, i.e. It could still use the value of monad-wrapped (string "Hello world."), When the user-defined function was called without additional parameters ( monad2.log() ). However, when the user-defined function was called using additional parameters ( monad2.log("foo", "bar") ), the code could no longer find value , although it could still use these additional parameters.

Update on the perplexity of the results: Partly because of the answer from @amon, I realized that the cryptic results do not appear because I change the number of parameters, but because I simply repeat the call to the lift ed method on the monad (did the number of parameters change). Thus, running monad2.log() twice in a line will give the correct value the first time, but it will be undefined a second time.

Questions

So why is a prototype needed in this code? Or, alternatively, how can the elimination of the cause of the prototype value be available several times, but not at another time?

Demo Code Description

The following are two versions of the code. The prototype code ( MONAD1 ) is identical to the code used by Crockford in his video, except that the user-defined console.log function is attached instead of alert so that I can play with this in node and not in the browser. In the code that does not support the prototype ( MONAD2 ), the changes indicated in the comments have been made. The result is displayed in the comments.

Prototype Usage 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" 

Prototype-free 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." ie still works monad2.log("foo", "bar"); // --> "undefined foo bar" ie ??? 

Jsbin

I 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 cryptic aspects of the results. (Jsbin doesn't seem to work if you just click “Run” on the console panel. Instead, you should activate the output panel by clicking the “Exit” tab, and then click “Run with js” in the “Exit” panel to see the results in the Console panel.)

+5
source share
1 answer

You must make a clear distinction between the specific type of monad and the instance of the monad that actually contains the meaning. The second example is a mixture of the two methods that I will discuss now.

First, the MONAD function creates a new type of monad. The concept of "monad" is not in itself. Instead, the function creates a type with monad-like behavior:

  • The unit operation wraps the value inside the monad. This is a kind of constructor: monadInstance = MonadType(x) . In Haskell: unit :: Monad m => a -> ma .
  • The bind operation applies the function to the value (s) in the monad instance. This function should return a monad of the same type. Then the bind operation returns a new monad: anotherMonadInstance = monadInstance.bind(f) . In Haskell: bind :: Monad m => ma -> (a -> mb) -> mb .

You can consider the operations of MonadType and unit() more or less the same. The reason we are creating a separate prototype is because we don’t want to inherit random baggage from the “function” type. In addition, hiding it inside a constructor like monad, we protect it from uncontrolled access - only lift can add new methods.

The lift operation is not essential, but very convenient. It allows you to use functions that work with equal values ​​(rather than monad instances), which will be used instead of monad instances. Usually it returns a new function that works at the monad level: functionThatReturnsAMonadInstance = lift(ordinaryFunction) . In Haskell: lift :: Monad m => (a -> b) -> (a -> mb) . But which lift monad should return? To maintain this context, each function raised is bound to a specific MonadType . Note: not only specific monadInstance ! When the function is raised, we can apply it to all monads of the same type.

Now I rewrote the code to make these conditions clearer:

 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, you get rid of monadInstance . Instead, you add the bind operation to MonadType ! This bind operation refers to the last value that was wrapped with unit() .

Now note that the return value of the removed functions is wrapped as a monad with unit .

  • When you create monadInstance (actually MonadType ), then MonadType.bind() refers to the value "Hello World" .
  • You call the canceled function log() . It gets the value in the monad, which is the last value that unit() wrapped, which is equal to "Hello World" . The return value of the raised function ( console.log ) is wrapped in unit() . This return value is undefined . Then you replace the bind function with a new bind that references the undefined value.
  • You call the canceled function log() . It gets the value in the monad, which is the last value that was wrapped in unit() , which is undefined . The observed result follows.
+2
source

Source: https://habr.com/ru/post/1259241/


All Articles