【问题标题】:Why are Promises Monads?为什么是 Promise Monad?
【发布时间】:2018-01-24 12:15:48
【问题描述】:

我一直在学习函数式编程,并且遇到过 Monads、Functors 和 Applicatives。

据我了解,适用以下定义:

a) ( A=>B ) => C[A] => C[B] |函子

b) ( A=>C[B] ) => C[A] => C[B] |单子

c) ( C[A=>B] ) => C[A] => C[B] |适用性

(参考:https://thedet.wordpress.com/2012/04/28/functors-monads-applicatives-can-be-so-simple/

此外,我知道 Monad 是 Functor 的一个特例。如,它应用一个函数返回一个包装的值到一个包装的值并返回一个包装的值。

当我们使用 Promise.then(func) 时,我们将传递给 Promise(即 C[A])的函数通常具有签名 A => B 并返回另一个 Promise(即 C[B])。所以我的想法是,Promise 只会是 Functor 而不是 Monad,因为 func 返回 B 而不是 C[B]。

但是,通过谷歌搜索我发现 Promise 不仅是 Functor,而且还是 Monad。我想知道为什么,func 不返回包装值 C[B],而只返回 B。我错过了什么?

【问题讨论】:

  • Promise 不是 Functor 或 Monad。
  • Promise 很容易成为函子/单子,但不幸的是它们不是。
  • @LukaszWiktor 在引用其他网站时,指出cross-posting is frowned upon 通常会有所帮助
  • 首先,您所说的定义(以及在您链接的博客文章中给出的定义)根本不完整 - 函子不是只是 (a -> b) -> C a -> C b 类型的函数;它也是适用于所述函数的一组法则(同样适用于应用函子和单子——此外,这些还需要一个函数a -> C a)。你说“谷歌搜索我发现......”但不是你在哪里找到这个说法,或者那里的推理是什么。
  • @zerkms Promises 正确实现了bind,但不幸的是由于它们的递归扁平化,没有正确实现mappure。然而,它们确实形成了一个有效的 applicative monadic functor,覆盖了 non-thenable 类型的受限类别。

标签: javascript functional-programming monads es6-promise functor


【解决方案1】:

日期。 查看这个新库 提供函子和单子运算符 对于普通的基于回调的函数 没有与theneables有关的问题:

https://github.com/dmitriz/cpsfy


JS Promise 既不是 Functor 也不是 Applicative 也不是 Monad

它不是函子,因为 composition preservation law (将功能组合发送到其图像组合) 被违反:

promise.then(x => g(f(x))) 

不等于

promise.then(f).then(g)

这实际上意味着什么, 重构永远不安全

promise
  .then(x => f(x))
  .then(y => g(y))

promise
  .then(x => g(f(x))

Promise 本来就是一个函子。

违反函子定律的证明。这是一个反例:

//函子组成保存律: // promise.then(f).then(g) 与 promise.then(x => g(f(x))) // f 接受函数 `x` // 并将其保存在 `then` 属性下的对象中: const f = x => ({then: x}) // g 从对象返回 `then` 属性 const g = obj => obj.then // h = compose(g, f) 是恒等式 常量 h = x => g(f(x)) // 使用身份函数履行承诺 常量承诺 = Promise.resolve(a => a) // 这个promise是通过identity函数实现的 promise.then(h) .then(res => { console.log("then(h) 返回:", res) }) // => "then(h) 返回:" a => a // 但是这个承诺永远不会实现 承诺.then(f) .then(g) .then(res => { console.log("then(f).then(g) 返回:", res) }) // => ??? // 因为这个不是: 承诺.then(f) .then(res => { console.log("then(f) 返回:", res) })

这是 Codepen 上的示例: https://codepen.io/dmitriz/pen/QrMawp?editors=0011

说明

由于组合h是恒等函数,所以promise.then(h)简单地采用promise的状态,它已经用恒等a => a实现了。

另一方面,f 返回所谓的thenable

1.2。 “thenable”是定义 then 方法的对象或函数。

为了维护函子定律,.then 必须简单地将结果 f(x) 包装成 promise。相反,当.then 中的函数返回“thenable”时,Promise Spec 需要不同的行为。根据2.3.3.3,存储在then键下的身份函数id = a => a被称为

id(resolvePromise, rejectPromise)

其中resolvePromiserejectPromise 是promise 解析过程提供的两个回调函数。但是,为了解决或拒绝,必须调用这些回调函数之一,这永远不会发生!因此,生成的 promise 仍处于待处理状态。

结论

在这个例子中, promise.then(x => g(f(x))) 用身份函数a => a 实现, 然而 promise.then(f).then(g) 永远处于待处理状态。 因此这两个承诺不等价 因此违反了函子定律。


Promise 既不是 Monad 也不是 Applicative

因为即使是 Pointed Functor 规范中的自然变换法则,也就是 Applicative(同态法则)的一部分,也被违反了:

Promise.resolve(g(x)) is NOT equivalent to Promise.resolve(x).then(g)

证明。这是一个反例:

// 标识函数保存在 `then` 属性下 const v = ({then: a => a}) // `g` 从对象返回 `then` 属性 const g = obj => obj.then // `g(v)` 是恒等函数 Promise.resolve(g(v)).then(res => { console.log("resolve(g(v)) 返回:", res) }) // => "resolve(g(v)) 返回:" a => a // `v` 被解包成永远挂起的 Promise // 因为它从不调用任何回调 Promise.resolve(v).then(g).then(res => { console.log("resolve(v).then(g) 返回:", res) }) // => ???

Codepen 上的这个例子:https://codepen.io/dmitriz/pen/wjqyjY?editors=0011

结论

在这个例子中,一个promise再次实现,而另一个promise正在等待,因此两者在任何意义上都不等同,违反了法律。


更新。

“成为 Functor”到底是什么意思?

Promise 成为 Functor/Applicative/Monad 与通过更改其方法或添加新方法使其成为 之间似乎存在混淆.然而,一个 Functor 必须已经提供了一个 map 方法(不一定在这个名称下),并且作为一个 Functor 显然取决于这个方法的选择。方法的实际名称没有任何作用,只要符合规律即可。

对于 Promises,.then 是最自然的选择,它不符合函子定律,如下所述。据我所知,其他任何 Promise 方法都不会以任何可以想象的方式使其成为 Functor。

更改或添加方法

其他方法是否可以定义符合法律规定则另当别论。我知道在这个方向上的唯一实现是由creed library 提供的。

但是要付出相当大的代价:不仅需要定义全新的map 方法,而且还需要更改promise 对象本身:creed promise 可以持有一个“theneable”作为值,而原生 JS Promise 不能。这一变化是实质性的,也是必要的,以避免违反示例中的法律,如下所述。特别是,我不知道有任何方法可以在没有这些根本性改变的情况下将 Promise 变成 Functor(或 Monad)。

【讨论】:

  • Promise 很容易形成一个 monad。为什么你认为 then 需要成为函子,而 then+resolve 需要成为 monad?
  • @Bergi 如果你能把它变成一个单子,请写一个可行的实现。 ;)
  • @Bergi thenresolve 是 API 提供的,并在问题中提到。当人们将 promise 称为 monad 时(错误地),他们指的是所提供的。您是否建议使用 API 中的其他方法?
  • 错误答案。当我们将“在then 道具下”替换为foo 道具下时,法律就得到了满足。 Monad 法则是不要破解编程语言中的保留字。
  • .@KenOKABE 你说得对,但then 不是 JS 中的保留字,是吗?虽然我相信(我对 monad 和朋友们的理解非常有限)OP 在严格的数学意义上是正确的,但 Promise 在实际意义上仍然可以作为 monad 工作。 Tomas Petricek 在他的优秀论文What we talk about when we talk about monads 中讨论了同样的观点差异。虽然有些东西在形式(数学)层面上不是单子,但从实现的角度来看,它仍然可以有效地成为单子。
【解决方案2】:

Promise(a lot like) 一个 monad,因为 then 已重载。

当我们使用 Promise.then(func) 时,我们向 Promise(即 C[A])传递了一个通常具有签名 A => B 的函数并返回另一个 Promise(即 C[B])。所以我的想法是,Promise 只会是 Functor 而不是 Monad,因为 func 返回 B 而不是 C[B]。

then(Promise<A>, Func<A, B>) : Promise<B> 也是如此(如果您原谅我的 javascript 类型伪代码,我将描述函数,就好像 this 是第一个参数)

不过,Promise API 为 then 提供了另一个签名,then(Promise<A>, Func<A, Promise<B>>) : Promise<B>。这个版本显然符合 monadic bind (>>=) 的签名。自己试试吧,效果不错。

但是,为 monad 拟合签名并不意味着 Promise monad。它还需要满足单子的代数定律。

单子必须满足的法则是结合律

(m >>= f) >>= g ≡ m >>= ( \x -> (f x >>= g) )

以及左右身份的规律

(return v) >>= f ≡ f v
m >>= return ≡ m

在 JavaScript 中:

function assertEquivalent(px, py) {
    Promise.all([px, py]).then(([x, y]) => console.log(x === y));
}

var _return = x => Promise.resolve(x)
Promise.prototype.bind = Promise.prototype.then

var p = _return("foo")
var f = x => _return("bar")
var g = y => _return("baz")

assertEquivalent(
    p.bind(f).bind(g),
    p.bind(x => f(x).bind(g))
);

assertEquivalent(
    _return("foo").bind(f),
    f("foo")
);

assertEquivalent(
    p.bind(x => _return(x)),
    p
);

我认为任何熟悉 Promise 的人都可以看出所有这些都应该是正确的,但您可以自己尝试一下。

因为Promise 是一个monad,我们可以派生ap 并从中得到一个应用程序,给我们一些非常好的语法和一点不明智的hackery:

Promise.prototype.ap = function (px) {
    return this.then(f => px.then(x => f(x)));
}

Promise.prototype.fmap = function(f) {
    return this.then(x => f(x));
}

// to make things pretty and idiomatic
Function.prototype.doFmap = function(mx) {
    return mx.fmap(this);
}

var h = x => y => x + y

// (h <$> return "hello" <*> return "world") >>= printLn
h.doFmap(_return("hello, ")).ap(_return("world!")).bind(console.log)

【讨论】:

  • 您的Function.prototype.fmap 已关闭。它确实需要Promise.prototype.fmap = Promise.prototype.then_return("hello, ").fmap(h).ap(…) - 或者如果您更喜欢静态函数而不是方法,Promise.fmap(h, _return("hello, ")).ap(…)
  • @Bergi 更正确和更通用,因为 fmap 实现属于“函子”。在这里,我主要是尝试与等效的 haskell 代码进行结构比较,为了使它更漂亮,我将过度专业化的 fmap 放在 Function 原型上。除非你说实现不正确?
  • 鉴于函数也是函子,这很容易混淆(在函数上调用 fmap 时,您会期望函数组合)。
  • Promise 不是单子,看我的回答stackoverflow.com/a/50173415/1614973
  • @DmitriZaitsev 答案错误。当我们将“在then 道具下”替换为foo 道具下时,法律就得到了满足。 Monad 法则是不要破解编程语言中的保留字。
【解决方案3】:

Promise 不是对包含 then 属性的对象的 Monad

Promises 将包含作为函数的 then 属性的对象视为特殊情况。因此,他们违反了左身份定律如下:

//Law of left identity is violated
// g(v) vs Promise.resolve(v).then(g)

// identity function saved under `then` prop
const v = ({then: x=>x({then: 1})})

// `g` returns the `then` prop from object wrapped in a promise
const g = (obj => Promise.resolve(obj.then))

g(v).then(res =>
          console.log("g(v) returns", res))
// "g(v) returns" x => x({ then: 1 })


Promise.resolve(v).then(g)
  .then(res =>
        console.log("Promise.resolve(v).then(g) returns", res))
// "Promise.resolve(v).then(g) returns" 1

example on codepen

这是因为 resolve 将 then 属性下的函数视为回调,将 then 链的延续作为参数传递,而不是创建包含它的 Promise。这样一来,它就不会像单元一样发挥作用,并导致违反单子定律。

但是,对于不包含 then 属性的值,它应该用作 monad。

【讨论】:

  • monad 基于与函数返回 monad 的链接。在 Promise 的情况下,这意味着返回 Promise,这是 theneables。因此,具有 then 属性的值是功能的核心,因此您不能排除它们。
【解决方案4】:

在我看来,Promise 是 Functor、Applicative Functor 和 Monad,因为它们遵守 functor 和 monad 法则。

好的,让我们研究一下仿函数的情况。为了让 Promises 成为 Functor 的一个实例,我们必须为 Promises 定义一个 fmap 函数 (a -&gt; b) - f a -&gt; f b 并且 fmap 应该通过 Functor 法则。什么是函子定律?

fmap id      = id
fmap (p . q) = (fmap p) . (fmap q)
  • id 是身份功能。我们可以像 var id = x =&gt; x 这样简单地在 JS 中实现它
  • (p . q) 中的. 是与 Math 中一样的组合运算。它本质上是 JS 中的 var dot = p =&gt; q =&gt; x =&gt; p(q(x))

JS 中的问题是对象,包括函数都是引用类型,这意味着与 Haskell 不同,每次你部分应用一个函数时,你会得到一个不同的函数做同样的事情。因此,以下法律中的公平检查将失败,但如果您检查结果值,它们将通过。

var id   = x => x,
    dot  = f => g => x => f(g(x)),
    fmap = f => p => p.then(v => f(v)),
    pr1 = Promise.resolve(1);
    
fmap(id)(pr1) === id(pr1); // false since objects are mutable
fmap(id)(pr1).then(v => console.log(v));
id(pr1).then(v=> console.log(v));

fmap(dot(x => x*2)(y => y+5))(pr1).then(v => console.log(v));
dot(fmap(x => x*2))(fmap(y => y+5))(pr1).then(v => console.log(v));

所以是的,Promise 是 Functor,如果您检查 Monad laws,您可以很容易地看出它们也是 Monad。

【讨论】:

  • return a &gt;&gt;= f ≡ f aa 本身是一个承诺时,此规则不适用,因为承诺不允许返回嵌套的承诺。
  • @zerkms 这完全符合...&gt;&gt;= 的类型是m a -&gt; (a -&gt; m a) -&gt; m b。所以f 的类型是a -&gt; m a(这意味着取一个简单的值并返回一个像promise 一样的单子值)。另一方面,return aPromise.resolve(1)(&gt;&gt;= f) 就像 JS .then(f) 所以如果 var f = x =&gt; Promise.resolve(x*3); Promise.resolve(1).then(f) 将返回与 f(1) 相同的承诺。 (由于 JS 引用类型,当然不是同一个对象)但是是的,Promise 满足一元法则。
  • Promises 既不是单子也不是函子。在 Promise 规范中,没有诸如 monad、functor 或任何其他范畴论中的概念之类的东西。仅仅因为它们包含相似的属性,并不意味着它们应该被这样对待。
  • then 有错误的类型(有点像 Promise a ~&gt; ? -&gt; Promise b),它被重载,它递归地变平,它吸收 then-ables,它自动提升函数。而且仅仅提供一些琐碎的例子并不能证明。
  • 您似乎误解了什么,例如fmap id = id 实际上意味着 - 这并不意味着对于一个特定的 x, fmap id x = id x;这意味着这个陈述应该是真的对于每个x。你还没有证明后一种说法。此外 - “如果你检查 Monad 定律,你可以很容易地看出它们也是 Monads” - 这无论如何都不构成“证明”。 OP 似乎在特别询问承诺是否形成单子,但您没有努力证明,甚至非正式地证明这个定理。
猜你喜欢
  • 1970-01-01
  • 2012-08-26
  • 1970-01-01
  • 1970-01-01
  • 2014-05-26
  • 2011-07-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-22
相关资源
最近更新 更多