JavaScriptのPromiseがモナドではない2つの理由
モチベーション
JavaScriptのPromiseがモナドではないという指摘は検索すればいくつか見つかるのだが、(個人的に)完全に納得できる説明は見つからなかった。細かい指摘はしないが、いずれの記事においてもやや厳密性が欠けているような気がしたからだ。
また、もう一つのモチベーションとしてScalaのTryがモナドではないという現象がJavaScriptのPromiseにも言えるように思えたのだが、この点を指摘している記事を見つけられなかったというのもある。
というわけで今回はJavaScriptのPromiseはモナドではない、という指摘を2つの観点からおこなってみたい。
概要
JavaScriptのPromiseは一見するとモナドのように扱えるが、実はモナド則に違反している。調べてみると then
メソッドの特殊な動作に着目してモナド則を壊せるという例がいくつかヒットするが、この記事では2つ目の方法として、例外を巡る仕組みを利用することでもモナド則を壊せることを紹介しよう。
モナド則についておさらい
まずはモナド則について知っておこう。Haskell Wikiを見るとモナド則は以下のようなものだとわかる。
return x >>= k = k x -- left identity
m >>= return = m -- right identity
(m >>= g) >>= h = m >>= (\x -> g x >>= h) -- associativity
Promiseで考える場合、return
をPromise.resolve
に、>>=
をPromise.prototype.then
に当てはめることになる。
なお、今回破壊するのは left identity だけなので他は忘れてもよい。
1つ目の方法: Promiseの入れ子に着目する
一見するとPromiseは left identity を満たしているように思える。
const k = (a) => Promise.resolve(`${a}, world!`)
// lhsとrhsは等価になる(left identityが成立する)
const lhs = Promise.resolve("hello").then(k)
const rhs = k("hello")
しかし、Promiseの入れ子を扱おうとすると崩壊することが知られている。left identityにおいてモナドを入れ子したケースでは
return (return a) >>= k = k(return a)
が成り立つことになるが、私が調べた限りJavaScriptではこの等式の左辺も右辺も正しく構成することができない(構成する方法があったら教えてください)。よってleft identityが破れると考えられる。
2つ目の方法: 例外の扱いに着目する
先に言っておくと、これはScalaにおいてTryがモナド則を満たさないことから着想している。次の例を見てほしい。
def k(a: Int): Try[Int] =
throw new Exception
val lhs = Success(0).flatMap(k)
val rhs = k(0)
このコードにおいてlhsはFailure[Exception]
という型の値になるが、rhsは例外を返す。TryにおいてSuccess
をreturn、flatMap
をbind(>>=)とみなすとlhsとrhsが等しくないことからleft identityが破れていることがわかる。
これと同じことがJavaScriptのPromiseでも成り立つと私は考える。PromiseはScalaのTryと同様に中で起きた例外をPromise型の値に落とせるからだ。
const k = (a) => {
throw new Error()
}
const lhs = Promise.resolve(0).then(k)
const rhs = k(0)
このJavaScriptコードではlhsはPromise型の値になるが、rhsは例外を返す。全く同じである。これでleft identityは破れた。
まとめ
JavaScriptのPromiseがモナドでないことを、全く異なる2通りの方法で説明した。片方は入れ子を巡る性質から、もう片方は例外を巡る性質からである。個人的には後者の理由の方がより深刻に思える(例外をラップするというのはPromiseにとって本質的な役割だからね)が、みなさんはいかがだろうか?
Discussion