はじめに
JavaScriptのPromise
は非同期処理を扱うための強力なツールとして広く利用されています。一方、関数型プログラミングにおけるモナドは、計算の文脈を扱う抽象的な構造として知られています。「JavaScriptのPromiseはモナドではない理由」について、既存の技術と比較しながら具体的な使用例を交えて解説します。
モナドとは何か
まず、モナドの基本的な概念を理解することが重要です。モナドは、関数型プログラミングにおいて計算を連鎖的に結合するためのデザインパターンです。モナドは以下の3つの要素を持ちます:
1. 型コンストラクタ
モナドは特定の型を持つ値を包むための型コンストラクタを提供します。
2. 単位関数(of
またはreturn
)
任意の値をモナドの文脈に持ち上げるための関数です。
3. バインド関数(chain
またはflatMap
)
モナドの文脈内で関数を適用し、その結果を新たなモナドとして返すための関数です。
モナド則
モナドは3つの重要な法則(モナド則)を満たす必要があります:
1. 左単位元則
of(a).chain(f) ≡ f(a)
2. 右単位元則
m.chain(of) ≡ m
3. 結合律
m.chain(f).chain(g) ≡ m.chain(x => f(x).chain(g))
JavaScriptのPromiseとは
Promise
はJavaScriptにおける非同期処理の結果を扱うためのオブジェクトです。then
メソッドを使用して非同期処理の完了後の操作を定義できます。
new Promise((resolve, reject) => {
// 非同期処理
}).then(result => {
// 結果の処理
});
Promiseがモナドと見なされる理由
一見すると、Promise
は値を包む型コンストラクタであり、then
メソッドで連鎖的な処理が可能なため、モナドとして扱えるように思えます。特に以下の点で共通点があります:
- 値を包む:非同期処理の結果を
Promise
オブジェクトとして包む。 - 関数の適用:
then
メソッドで結果に対する関数を適用できる。
Promiseがモナドではない理由
しかし、Promise
は厳密にはモナドの定義やモナド則を満たしていないため、モナドではないとされています。その理由を詳しく見ていきましょう。
1. 関数の遅延評価がない
モナドは一般的に遅延評価を前提としていますが、Promise
は作成時に即座に実行される性質があります。例えば:
const p = new Promise((resolve, reject) => {
console.log('実行されました');
});
このコードではPromise
の作成時に即座に「実行されました」とログが出力されます。遅延評価がないため、モナドの性質と異なります。
2. エラー処理の不整合
Promise
のcatch
メソッドはエラーを処理しますが、このエラー伝播の方法がモナド則の結合律に反する場合があります。すなわち、エラー時の挙動が一貫していないため、モナドとしての性質を満たしていません。
3. モナド則を満たしていない
先に挙げたモナド則のうち、Promise
は特に結合律を満たしません。具体的な例を考えてみましょう:
Promise.resolve(1)
.then(x => Promise.resolve(x + 1))
.then(x => Promise.resolve(x + 1));
このチェーンは期待通り動作しますが、エラーや非同期性を組み合わせると挙動が変わる可能性があります。これはモナド則の結合律に反しています。
他の技術との比較
モナドパターンを正確に実装したライブラリやフレームワークと比較すると、Promise
の違いが明確になります。
1. Monadライブラリの使用例
例えば、JavaScriptでモナドを実装したライブラリとして、Folktaleがあります。FolktaleのTask
モナドは遅延評価され、モナド則を満たしています。
const { task } = require('folktale/concurrency/task');
const computation = task(resolver => {
// 非同期処理
});
computation.run().listen({
onResolved: result => {
// 結果の処理
}
});
2. Observableとの比較
RxJSのObservable
も非同期データストリームを扱うための強力なツールですが、これもモナドではありません。しかし、flatMap
(mergeMap
)を使用してデータストリームを合成できるため、モナド的な操作が可能です。
import { of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
of(1)
.pipe(
mergeMap(x => of(x + 1))
)
.subscribe(result => {
console.log(result); // 2
});
まとめ
JavaScriptのPromise
は非同期処理を扱う上で非常に便利なオブジェクトですが、厳密な意味でのモナドではありません。これは、遅延評価の欠如やモナド則を満たしていないこと、エラー処理の不整合などが理由です。他のモナド実装や非同期処理ライブラリと比較することで、その違いが明確になります。
関数型プログラミングの概念をJavaScriptに適用する際には、これらの違いを理解し、適切なツールやデザインパターンを選択することが重要です。