【问题标题】: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 上的 herehere)。他在 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 首先存在的原因。效果/上下文决定了如何实现bindunit。克罗克福德的例子没有反映出这一点——或者至少我看不到。
  • @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

您可以将MonadTypeunit() 操作视为或多或少相同的东西。我们创建单独原型的原因是我们不想从“函数”类型中继承随机包袱。此外,通过将它隐藏在 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,我现在认为这是不正确的。我也误解了thisunit.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
相关资源
最近更新 更多