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

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

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

代码来源

我正在观看 Douglas Crockford 的名为 "Monads and Gonads" 的视频(在 YouTube here or here 上)。他在 JavaScript 中包含了一个 monad 的演示实现,如下所示。

monad 对象及其原型

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

需要原型吗?

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

移除原型后令人费解的结果

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

关于令人费解的结果的更新: 部分原因是@amon 的回答,我意识到因为我更改了数字而没有出现令人费解的结果的参数,而是因为我只是重复调用 monad 上的 lifted 方法(无论参数数量是否已更改)。因此,连续两次 运行 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"

非原型使用代码

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

我在 node 中玩过这段代码,但你可以在这个 jsbin 中看到结果。 Console.log 在 jsbin 中的工作似乎与在终端节点中的工作不完全相同,但它仍然显示结果的相同令人费解的方面。 (如果您只是在控制台窗格中单击 'Run',jsbin 似乎不起作用。相反,您必须通过单击 'Output' 选项卡激活输出窗格,然后单击 'Run with js' 在 'Output' 窗格中查看结果在 'Console' 窗格中。)

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

首先,MONAD函数构造了一个新的monad类型。 “monad”这个概念本身并不是一种类型。相反,该函数创建了一个具有类似 monad 行为的类型:

  • unit 操作将值包装在 monad 中。它是一种构造函数:monadInstance = MonadType(x)。在 Haskell 中:unit :: Monad m => a -> m a.
  • bind 操作将函数应用于 monad 实例中的值。该函数必须 return 是同一类型的 monad。然后绑定操作 returns 新的 monad:anotherMonadInstance = monadInstance.bind(f)。在 Haskell 中:bind :: Monad m => m a -> (a -> m b) -> m b.

您可以认为 MonadTypeunit() 操作或多或少是同一件事。我们创建一个单独的原型的原因是我们不想从“函数”类型继承随机包袱。此外,通过将它隐藏在 monad 类型的构造函数中,我们保护它免受未经检查的访问——只有 lift 可以添加新方法。

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

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

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"

您的代码中发生的事情是您摆脱了 monadInstance。相反,您将 bind 操作添加到 MonadType!这个 bind 操作恰好引用了用 unit().

包裹的 last

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

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