👊

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で考える場合、returnPromise.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