为什么 Promise 是 Monad?

IT技术 javascript functional-programming monads es6-promise functor
2021-03-06 06:13:30

我一直在学习函数式编程,并遇到过 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。我错过了什么?

4个回答

日期。请参阅这个新的库,证明 functor 和 monad 操作符是基于回调的简单函数,这些函数没有下面概述的 theneables 问题:

https://github.com/dmitriz/cpsfy


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

它不是函子,因为 违反组合保存法则(将函数的组合发送到其图像的组合):

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) vs promise.then(x => g(f(x)))

// f 接受函数`x` 
// 并将其保存在 `then` 属性下的对象中:
const f = x => ({然后: x})

// g 从对象返回 `then` 属性 
const g = obj => obj.then

// h = compose(g, f) 是身份
const h = x => g(f(x))

// 使用身份函数履行Promise
const promise = Promise.resolve(a => a)

// 这个promise 是通过identity 函数实现的
Promise.then(h)
       .then(res => {
           console.log("then(h) 返回:", res)
       })
// => "then(h) 返回:" a => a

// 但是这个Promise永远不会兑现
Promise.then(f)
       .then(g)
       .then(res => {
           console.log("then(f).then(g) 返回:", res)
       })
// => ???

// 因为这不是:
Promise.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必须简单地将结果包装到 promise 中f(x)相反,当内部函数返回“thenable”Promise Spec需要不同的行为.then根据2.3.3.3id = a => a存储在thenkey的身份函数被称为

id(resolvePromise, rejectPromise)

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

结论

在这个例子中, promise.then(x => g(f(x))) 由身份函数 完成a => a,而 promise.then(f).then(g) 永远保持挂起状态。因此这两个Promise是不等价的,因此违反了函子定律。


Promise 既不是Monad也不是Applicative

因为即使是来自Pointed Functor Spec的自然变换定律,也就是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/Applicative/Monad 与通过改变其方法或添加新方法来实现它的方法之间似乎存在混淆然而,一个 Functor 必须已经提供了一个map方法(不一定在这个名字下),作为一个 Functor 显然取决于这个方法的选择。方法的实际名称没有任何作用,只要满足规律即可。

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

更改或添加方法

是否可以定义其他符合规律的方法是另一回事我所知道的这个方向的唯一实现是由creed library提供的

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

Promise 很容易形成一个 monad。为什么你认为它then需要是一个函子,那个then+resolve需要创建一个 monad?
2021-04-16 06:13:30
.@KenOKABE 你说得对,但then不是 JS 中的保留字,是吗?虽然我相信(由于我对 monad 和朋友的理解非常有限)OP 在严格的数学意义上是正确的,但 promise 在实际意义上仍然像 monad 一样有效。Tomas Petricek 在他的优秀论文What we talk about monads 中讨论了同样的观点差异虽然某些东西在正式(数学)层面上不是 monad,但从实现的角度来看,它仍然可以有效地成为一个 monad。
2021-04-18 06:13:30
@Bergithen并且resolve正是通过API提供的,而在问题中提到。当人们将 promise 称为 monad 时(错误地),他们指的是所提供的内容。您是否建议使用 API 中的其他方法?
2021-04-25 06:13:30
错误的答案。当我们将“under thenprop”替换为 under fooprop 时,这些定律就完全满足了。Monad 法则不是要破解编程语言中的保留字。
2021-04-28 06:13:30
@Bergi 如果你能把它变成一个 monad,请写一个可行的实现。;)
2021-05-01 06:13:30

Promise(很像)一个 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是第一个参数)

无极API用于提供另一个签名then虽然,then(Promise<A>, Func<A, Promise<B>>) : Promise<B>这个版本显然符合 monadic bind ( >>=)的签名自己试试看,确实有效。

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

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并得到一个应用程序,给我们一些非常好的语法和一些不明智的黑客:

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)
@DmitriZaitsev 错误答案。当我们将“under thenprop”替换为 under fooprop 时,这些定律就完全满足了。Monad 法则不是要破解编程语言中的保留字。
2021-04-17 06:13:30
鉴于函数也是函子,这简直令人困惑(在调用函数时您会期望函数组合fmap)。
2021-04-28 06:13:30
Promise不是 monad,请参阅我的回答stackoverflow.com/a/50173415/1614973
2021-04-28 06:13:30
@Bergi 这将是更正确和可推广的,因为 fmap 实现属于“函子”。在这里,我主要尝试与等效的 haskell 代码进行结构比较,为了使它更美观,我将过度专业化的 fmap 放在函数原型上。除非你说实现不正确?
2021-05-08 06:13:30
Function.prototype.fmap的关了。它确实需要Promise.prototype.fmap = Promise.prototype.then_return("hello, ").fmap(h).ap(…)- 或者如果您更喜欢静态函数而不是方法,Promise.fmap(h, _return("hello, ")).ap(…)
2021-05-14 06:13:30

Promise 不是对包含 then 属性的对象的单子

Promise 将包含 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

代码笔上的例子

发生这种情况是因为 resolve 将 then 属性下的函数视为回调,将 then 链的延续作为参数传入,而不是创建包含它的 promise。通过这种方式,它不像单元那样运行并导致违反单子定律。

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

monad 基于与函数返回 monad 的链接。在Promise的情况下,这意味着返回Promise,这是可行的。因此,带有 then 属性的值是功能的核心,因此您不能排除它们。
2021-05-09 06:13:30

在我看来,Promise 是函子、应用函子和单子,因为它们遵守函子和单子定律。

好的,让我们研究函子的情况。为了让 Promises 成为 Functor 的一个实例,我们必须为 Promises定义一个fmap函数(a -> b) - f a -> f bfmap通过 Functor 法则。什么是函子定律?

fmap id      = id
fmap (p . q) = (fmap p) . (fmap q)
  • id是恒等函数。我们可以像 var 一样简单地在 JS 中实现它id = x => x
  • .(p . q)是该组合物的操作,就像在数学式 它本质上是var dot = p => q => x => p(q(x))在 JS 中。

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));

所以是的,Promises 是 Functors,如果你检查Monad 法则,你可以很容易地知道它们也是 Monads。

Promises 既不是 monadic 也不是函子。在 promises 规范中没有来自范畴论的 monad、functor 或任何其他概念。仅仅因为它们包含相似的属性,并不意味着它们应该被如此对待。
2021-04-28 06:13:30
return a >>= f ≡ f a此规则不适用于aPromise 本身,因为 Promise 不允许返回嵌套的 Promise。
2021-05-01 06:13:30
then有错误的类型(有点像Promise a ~> ? -> Promise b),它被重载,它递归地展平,它吸收 then-ables,它自动提升功能。仅仅提供一些琐碎的例子并不能提供任何证据。
2021-05-09 06:13:30
您似乎误解了 eg 的fmap id = id实际含义 - 这并不意味着对于某个特定的x, fmap id x = id x这意味着这个陈述应该对每个 x 都成立你还没有证明后一种说法。此外——“如果你检查 Monad 法则,你可以很容易地看出它们也是 Monad”——这在任何方面都不构成“证明”。OP 似乎专门询问 promise 是否形成 monad,但您没有努力证明,甚至非正式地证明这个定理。
2021-05-12 06:13:30
@zerkms 这完全成立......类型>>=m a -> (a -> m a) -> m b所以类型fa -> m a(这意味着取一个简单的值并返回一个像promise这样的monadic值)。另一方面return aPromise.resolve(1)并且(>>= f)就像 JS.then(f)所以 if var f = x => Promise.resolve(x*3); Promise.resolve(1).then(f)将返回相同的Promisef(1)(由于 JS 引用类型,当然不是同一个对象)但是 Promise 满足一元定律。
2021-05-15 06:13:30