为什么Douglas Crockford的monad演示代码需要monad原型?

And*_*ems 5 javascript monads prototype

注意:虽然这个问题中的代码涉及函数式编程/ monad等,但我不是在询问函数式编程(我也不认为这个问题应该有与函数式编程相关的标签等).相反,我问的是使用JavaScript的原型.

代码来源

我正在观看道格拉斯·克罗克福德的视频"Monads and Gonads"(在YouTube上这里这里).他在JavaScript中包含了monad的演示实现,如下所示.

monad对象和它的原型

在他的代码中,他创建了一个真正空的对象,Object.create(null)并使用它作为最终monad对象的原型.他将bind方法附加到monad对象本身,但是后来附加到monad使用的任何自定义函数都不lift是附加到monad对象本身而是附加到它的原型.

需要原型吗?

在我看来,使用原型是不必要的复杂性.为什么这些自定义函数不能直接附加到monad对象本身?然后,在我看来,不需要原型,我们可以简化代码.

删除原型时令人费解的结果

我尝试实现这种简化并得到令人费解的结果.非原型使用代码有时仍然有效,即当调用自定义函数而没有额外的参数(monad2.log())时,它仍然可以使用monad-wrapped值(字符串"Hello world." ).但是,当使用额外的参数(monad2.log("foo", "bar"))调用自定义函数时,代码现在无法找到,value即使它仍然可以使用这些额外的参数.

关于令人费解的结果的更新:部分是因为来自@amon的回答,我意识到令人费解的结果没有出现因为我改变了参数的数量,而是因为我只是lift在monad上重复调用ed方法(无论是或者参数的数量是否已经改变).因此,连续运行monad2.log()两次将在第一次产生正确的值,但第二次将不确定.

问题

那么,为什么这段代码需要原型?或者,或者,如何消除原型导致value可以访问某些时间而不是其他时间?

演示代码说明

代码的两个版本如下所示.原型使用代码(MONAD1)与Crockford在他的视频中使用的代码相同,只是附加的自定义函数console.log不是为了alert让我可以在节点而不是在浏览器中使用它.非原型使用代码(MONAD2)进行注释中指示的更改.输出显示在注释中.

原型使用代码

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"
Run Code Online (Sandbox Code Playgroud)

非原型使用代码

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. ???
Run Code Online (Sandbox Code Playgroud)

JSBin

我已在节点中使用此代码,但您可以在此jsbin中查看结果.Console.log似乎在jsbin中的工作方式与终端中的节点完全相同,但它仍然显示了结果的同样令人费解的方面.(如果您只是单击控制台窗格中的"运行",则jsbin似乎不起作用.而是必须通过单击"输出"选项卡激活输出窗格,然后单击"使用js运行" "输出"窗格可在"控制台"窗格中查看结果.)

amo*_*mon 2

您必须明确区分特定类型的 monad 和实际包含值的 monad 实例。您的第二个示例是以我稍后将讨论的方式将两者混合在一起。

\n\n

首先,该MONAD函数构造一个新的 monad 类型。概念 \xe2\x80\x9cmonad\xe2\x80\x9d 本身并不是一种类型。相反,该函数创建一个具有类似 monad 行为的类型:

\n\n
    \n
  • unit操作将一个值包装在一个 monad 内。它是一种构造函数:monadInstance = MonadType(x)。在哈斯克尔中:unit :: Monad m => a -> m a.
  • \n
  • bind操作将函数应用于 monad 实例中的值。该函数必须返回相同类型的 monad。然后绑定操作返回新的 monad: anotherMonadInstance = monadInstance.bind(f)。在哈斯克尔中:bind :: Monad m => m a -> (a -> m b) -> m b.
  • \n
\n\n

您可以将MonadTypeunit()操作视为或多或少相同的事情。我们创建单独原型的原因是我们不想从 \xe2\x80\x9cfunction\xe2\x80\x9d 类型继承随机包袱。此外,通过将其隐藏在 monad 类型构造函数中,我们可以保护它免受未经检查的访问 \xe2\x80\x93 只能lift添加新方法。

\n\n

操作lift不是必需的,但非常方便。它允许对普通值(不是 monad 实例)起作用的函数应用于 monad 实例。通常,它会返回一个在 monad 级别运行的新函数:functionThatReturnsAMonadInstance = lift(ordinaryFunction)。在哈斯克尔中:lift :: Monad m => (a -> b) -> (a -> m b). 但是哪种单子lift应该返回呢?为了保留此上下文,每个提升的函数都绑定到特定的MonadType. 注意:不仅仅是一个特定的monadInstance!一旦一个函数被提升,我们就可以将它应用于同一类型的所有 monad。

\n\n

我现在将重写代码以使这些术语更加清晰:

\n\n
function CREATE_NEW_MONAD_TYPE() {\n    var MonadType = Object.create(null);\n    function unit (value) {\n        var monadInstance = Object.create(MonadType);\n        monadInstance.bind = function (func, ...args) {\n            return func(value, ...args);\n        }\n        return monadInstance;\n    }\n    unit.lift = function (name, func) {\n        MonadType[name] = function (...args) {\n            return unit(this.bind(func, ...args));\n        };\n        return unit;\n    };\n    return unit;\n}\n\nvar MyMonadType = CREATE_NEW_MONAD_TYPE()\nMyMonadType.lift(\'log\', console.log);  // adds MyMonadType(\xe2\x80\xa6).log(\xe2\x80\xa6)\n\nvar monadInstance = MyMonadType("Hello world.");\n\nmonadInstance.log();             // --> "Hello world."\nmonadInstance.log("foo", "bar"); // --> "Hello world. foo bar"\n
Run Code Online (Sandbox Code Playgroud)\n\n

您的代码中发生的情况是您摆脱了monadInstance. 相反,您可以bindMonadType!添加一个操作。此bind操作恰好引用了用 包裹的最后一个unit()值。

\n\n

现在请注意,提升函数的返回值被包装为带有 的 monad unit

\n\n
    \n
  • 当您构造 the monadInstance(实际上是MonadType)时,thenMonadType.bind()指的是该"Hello World"值。
  • \n
  • 您调用提升的log()函数。它接收 monad 中的值,这是用 包裹的最后一个值unit(),即"Hello World"。提升函数 ( ) 的返回值console.log用 包裹起来unit()。这个返回值为undefined. 然后,您可以用引用该值的bind新函数替换该函数。bindundefined
  • \n
  • 您调用提升的log()函数。它接收 monad 中的值,这是用 包裹的最后一个值unit(),即undefined。观察到的输出随之而来。
  • \n
\n