JavaScriptのPromiseはモナドではない理由

はじめに

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. エラー処理の不整合

Promisecatchメソッドはエラーを処理しますが、このエラー伝播の方法がモナド則の結合律に反する場合があります。すなわち、エラー時の挙動が一貫していないため、モナドとしての性質を満たしていません。

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も非同期データストリームを扱うための強力なツールですが、これもモナドではありません。しかし、flatMapmergeMap)を使用してデータストリームを合成できるため、モナド的な操作が可能です。

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に適用する際には、これらの違いを理解し、適切なツールやデザインパターンを選択することが重要です。

参考文献

Posted In :