【问题标题】:Why is a monad prototype required for Douglas Crockford's monad demo code?为什么 Douglas Crockford 的 monad 演示代码需要 monad 原型?
【发布时间】:2016-11-03 16:34:57
【问题描述】:

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

代码来源

我正在观看 Douglas Crockford 的名为“Monads and Gonads”的视频(在 YouTube 上的 here 或 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,这样我就可以在 node 中而不是在浏览器中使用它。非原型使用代码 (MONAD2) 进行了 cmets 中指示的更改。输出显示在 cmets 中。

使用原型的代码

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 中的工作方式似乎与在终端中的 node 中的工作方式不同,但它仍然显示出同样令人费解的结果方面。 (如果您只单击控制台窗格中的“运行”,jsbin 似乎不起作用。相反,您必须通过单击“输出”选项卡来激活输出窗格,然后单击“使用 js 运行” 'Output' 窗格以在 'Console' 窗格中查看结果。)

【问题讨论】:

  • 这与Object.create() 和unit.lift() 以及unit(this.bind(func, args)) 中提到的this 的相对时间有关。在我回答之前,我需要更多地使用它来解释原因
  • 这是一个奇怪而混乱的 monad 实现。您应该查看Fantasy Land。各种 monad 有很多简洁明了的实现。
  • @ftor,我听说过 Fantasy Land 并将对其进行研究。为了让 Crockford 从怀疑中受益(他可能应该或可能不应该得到,我不知道),我猜他的 monad 实现很奇怪为了提供功能而故意混淆-programming-newbie JavaScript 开发人员是一个过于简单且可能已损坏的起点,只是为了向我们介绍 monad。如果您认为即使是对 JS 中 monad 的简单介绍也应该以不同的方式进行,请随时再次插话,但与此同时,我将继续将其拆开以开始“深入”FP。
  • @Andrew 首先,我认为您想了解 FP 真是太好了。我也是一个学习者。无论如何,Monad 由一个类型(在 JS 中通常是带有一些附加方法的原型)、一个表示该类型实例的复合类型(在 JS 中通常是 Object 类型)和一个特定的效果/上下文组成,即这个特定的 Monad 首先存在的原因。效果/上下文决定了如何实现bind 和unit。克罗克福德的例子没有反映出这一点——或者至少我看不到。
  • @ftor,写了一个特定的效果/上下文...如果我理解正确,Crockford 在他的视频中的其他地方讨论了这些事情。例如,他将function MONAD() {... 更改为function MONAD(modifier) {... 以允许修饰函数改变单子/类型。我在这里展示的代码只是视频的一部分。正如您所建议的,他的“也许”单子修改了bind,以便在空值的情况下简单地返回单子实例。无论如何,您还给了我有用的类别:类型、复合类型、特定效果/上下文。这有助于我对其中一些概念进行分类。再次感谢。

标签: javascript prototype monads


【解决方案1】:

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

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

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

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

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

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

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 值。

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

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

【讨论】:

  • 哇,您的回答在很多方面都有帮助。无论如何,当我第一次发布问题时,我不知何故错误地认为任何MonadType 只会用于以某种单例模式实例化单个monadInstance,我现在认为这是不正确的。我也误解了this 在unit.lift 中指的是什么,即如果命名函数由monadInstance(它必须是)调用,那么this 指的是monadInstance 而不是MonadType。除其他外,您的答案是关于命名变量值的案例研究。
  • 提个醒:在我最初的问题中,我应该将args 的所有实例都写为...args。 (否则代码无法正常工作。)我已经编辑了我的问题以反映这一点,进行了 2 处更改。您刚刚复制了我的代码,包括那些错误,因此您现在可能想要编辑您的代码答案以包含这些省略号。 (哦,您在一个“monadInstane”中忘记了一个“c”。)在您进行这些更改后,我将删除此评论。 (我不希望我的名字,即 OP 的名字,作为你答案的编辑。只是看起来很糟糕。)
  • @AndrewWillems 我根据您的建议修复了答案,谢谢。我认为该代码是将 monad 编码为动态类型语言的一种非常优雅的编码方式,但它并不明显。重点似乎不在于可教性,而在于彻底的封装。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-04
  • 2011-07-01
  • 2018-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-21
相关资源
最近更新 更多