なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

モナド則からJavaScriptのPromiseがモナドであることを確かめてみた

2022/1/13 型コンストラクタを「抽象型」としていたのを「多相型」に修正しました

最近になって、プログラミングにおけるモナドには「モナド」という規則があることを知り、これを使えばこれまた最近聞いた「JavaScriptのPromiseはモナドである」というのが本当かどうか確かめられるのでは?と思ったので確かめてみた。

注意事項

筆者はモナド初心者であるため、この記事を読んで「納得できる」ないし「理解できる」とは限らないことをご了承いただければ幸いである(マサカリを投げたりしていただいても構いません)。

モナド則って何よ?

モナド則とは対象mモナドであるために成り立っていなければならない規則である。つまりこのモナド則が成り立たない対象mモナドではないことを示し、裏を返せば、モナド則が成り立つ対象mモナドとなる。

モナドの形式的定義

モナド則について説明する前に、モナドの形式的な定義について説明せねばなるまい。モナドの形式的定義は以下の3つから成る。

要素 型(カインド) 概要
型コンストラクM * -> * モナドとなる多相型*1
return関数*2*3 a -> M a aの値を型M aの値に変換する関数*4
(>>=)演算子*5 M a -> (a -> M b) -> M b M aの値から型aの値を抽出し、型M bの値に変換する関数を適用する関数

つまり、モナドである型には型コンストラクMreturn関数、(>>=)演算子の3つもしくはそれらに類似する機能がほぼ確実にあると言っても過言ではない。

3つのモナド

そしてモナド則の話に戻るが、モナドの形式的定義があるからと言って、それがモナドであるとは限らない。モナドであるためには、定式的定義だけでなく、モナド則が成り立ってなければならない。
モナド則は、以下の3つのとおりである。

  • モナド則1: return(x) >>= f ≡ f(x)

return関数にxを適用し得たモナドから値を抽出し『値からモナドを返す』関数fに適用する」ことは「fに直接xを適用する」ことと同じでなければならない

モナドmから値を抽出しreturn関数に適用した結果はmと同じでなければならない

  • モナド則3: (m >>= f) >>= g ≡ m >>= (λx. (f(x) >>= g))

モナドmから関数f→関数gの順に連続で値を抽出し適用していく」ことは「モナドmから値を抽出し、『fに適用した結果から値を抽出しgに適用する』関数に適用すること」と同じでなければならない。


以上が、自分なりに解釈した「モナド則」である。

JavaScriptのPromiseがモナドであるかどうか確認

では早速JavaScriptのPromiseがモナドであるかどうか、モナド則から確認してみる。JavaScriptのPromiseについての詳細は、以下を参照されたし。
developer.mozilla.org

まず、Promiseの何がモナドの形式的定義のうちどれに当てはまっているのかについては、以下がそれではないかと考えられる。

要素 該当するPromiseの機能 解説
M Promise 言わずもがな
return Promise.resolveなど 値を適用した結果のPromiseを返す
(>>=) Promise.prototype.thenなど 結果から値を適用した結果のPromiseを返す

つまり実際にPromiseがモナドであると言うならば、

printLn = λx. 標準出力関数(x)
return(1) >>= printLn

は、JavaScriptのPromiseで言う

const consoleLog = x => Promise.resolve(console.log(x));
Promise.resolve(1).then(consoleLog);

と一緒である、という前提で話を進める。

モナド則1

モナド則1はreturn(x) >>= f ≡ f(x)である。つまりJavaScriptのPromise的には、Promise.resolve(x).then(f) ≡ f(x)となるはずである*6

const f = x => Promise.resolve(console.log(String(x)));
Promise.resolve(42).then(f);

"42"

const f = x => Promise.resolve(console.log(String(x)));
f(42);

"42"

結果がまったく同じだったため、モナド則1は満たしていることが確認できた。

モナド則2

モナド則2はm >>= return ≡ mである。つまりJavaScriptのPromise的には、promise.then(x => Promise.resolve(x)) ≡ promiseとなるはずである*7

const printPromise = p => p.then(n => console.log(n));
const promise = Promise.resolve(42);

// Promiseの出力だけでは同じ結果なのか判断できないため、値を出力する
printPromise(promise.then(x => Promise.resolve(x)));

42

const printPromise = p => p.then(n => console.log(n));

// Promiseの出力だけでは同じ結果なのか判断できないため、値を出力する
printPromise(Promise.resolve(42));

42

結果がまったく同じだったため、モナド則2も満たしていることが確認できた。

モナド則3

モナド則3は(m >>= f) >>= g ≡ m >>= (λx. (f(x) >>= g))である。つまりJavaScriptのPromise的には、(promise.then(f)).then(g) ≡ promise.then(x => (f(x).then(g)))となるはずである*8

const f = x => Promise.resolve([x]);
const g = x => Promise.resolve(console.log(x));
const promise = Promise.resolve(42);

(promise.then(f)).then(g);

Array [42]

const f = x => Promise.resolve([x]);
const g = x => Promise.resolve(console.log(x));
const promise = Promise.resolve(42);

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

Array [42]

結果がまったく同じだったため、モナド則3も満たしていることが確認できた。

結論

以上の結果から、JavaScriptのPromiseでモナド則が成り立つ*9ことがわかったので、「JavaScriptのPromiseはモナドである」という情報が自分の中でより確実なものとなった。

*1:いわゆるジェネリクスのことであり、あらゆる型を"引数"として取り構成される型

*2:unit関数とも呼ばれる

*3:プログラミング言語におけるreturnとは異なるため注意

*4:ここでいう関数は写像的な意味合いに近い

*5:bind関数とも呼ばれる

*6:ただし、fの結果はPromiseであるとする

*7:本来Promise.resolveを直接渡せば良いはずだが、正常に動かないのでPromiseを返すアロー関数で対処(この観点から、Promiseはモナドではないと考えることもできるかもしれないが、今回はこれが正常だと考えて話を進めることにする)

*8:ただし、f, gの結果はPromiseであるとする

*9:モナド則2は若干怪しい結果ではあるが