はじめに
JavaScriptのPromiseは、非同期処理を扱うための強力な機能として広く利用されています。一方、関数型プログラミングにおいて重要な概念であるモナド(Monad)とPromiseを比較する議論がよく見られます。しかし、Promiseはモナドではないという指摘もあります。本記事では、JavaScriptのPromiseがなぜモナドではないのかを説明し、既存の技術と比較しながら具体的な使用例を交えて解説します。
モナドとは何か
関数型プログラミングにおけるモナド
モナドは、関数型プログラミングにおいて副作用を扱うためのデザインパターンです。モナドは3つの要素、すなわち「単位元」(unitまたはreturnとも呼ばれる)、「結合」(bindまたはflatMap)、および「モナド法則」を満たす必要があります。これにより、純粋関数型の文脈で副作用を安全に扱うことが可能となります。
Promiseとモナドの比較
Promiseの基本的な構造
Promiseは、JavaScriptにおける非同期処理の結果を表すオブジェクトです。非同期処理の成功時と失敗時のコールバックを登録し、処理の完了を待つことができます。Promiseはthen
メソッドを持ち、非同期処理のチェーンを構築することが可能です。
表面的な類似点
Promiseのthen
メソッドは、次の処理を続けて行うために使用されます。これは、モナドのbind
操作に似ており、一見するとPromiseはモナドであるかのように見えます。
なぜPromiseはモナドではないのか
モナド法則の違反
モナドであるためには、以下のモナド法則を満たす必要があります:
- 左単位元則(Left Identity):
unit(a).bind(f) ≡ f(a)
- 右単位元則(Right Identity):
m.bind(unit) ≡ m
- 結合則(Associativity):
m.bind(f).bind(g) ≡ m.bind(x => f(x).bind(g))
JavaScriptのPromiseは、これらのモナド法則を完全には満たしません。特に、エラーハンドリングや評価のタイミングに関して、Promiseの動作はモナドの期待する動作と一致しないことがあります。
評価の即時性
モナドは遅延評価を前提とすることが多いですが、Promiseは作成された時点で即座に実行されます。これは、計算の開始時期を制御できないことを意味し、モナドの性質と異なります。
例:Promiseの評価タイミング
以下のコードを考えてみましょう。
const promise = new Promise((resolve, reject) => {
console.log('Promiseが実行されました');
resolve(42);
});
このコードでは、Promiseオブジェクトを作成した瞬間に「Promiseが実行されました」とログが出力されます。これは、Promiseが遅延評価ではなく即時評価されることを示しています。モナドであれば、bind
(またはthen
)が呼ばれるまで計算は開始されないべきですが、Promiseはそうではありません。
エラーハンドリングの差異
モナドは通常、エラーが発生した場合でも計算を続行するためのメカニズムを提供します。しかし、Promiseは一度拒否(reject)されると、その後のthen
チェーンはスキップされ、catch
ハンドラに移行します。これは、モナドの結合則を破る可能性があります。
既存の技術との比較
Arrayモナドとの比較
JavaScriptの配列(Array)は、モナドの性質を持つとされています。配列はmap
やflatMap
(Array.prototype.flatMap
)メソッドを持ち、これらを使用してモナド法則を満たす操作が可能です。配列の場合、評価は遅延されませんが、計算の組み合わせにおいてモナドの特性を示します。
Fantasy Land仕様のモナド
JavaScriptコミュニティでは、モナドや他の代数的構造を実装するための標準インターフェースとして「Fantasy Land」仕様が提案されています。これは、モナドが満たすべき法則とメソッドのシグネチャを定義しています。Promiseはこの仕様に完全には適合しておらず、モナドとして扱うことができません。
使用例:Promiseの制限と代替策
Promiseの制限
Promiseはその評価の即時性やエラーハンドリングの挙動により、モナドとしての一貫性を欠く場合があります。特に、計算の開始時期を遅延させたい場合や、エラーを含めた計算の連鎖をモナド法則に従って行いたい場合、Promiseでは適切に対応できないことがあります。
代替策としてのTaskモナド
これらの制限を克服するために、モナド法則を満たす非同期処理の代替として「Taskモナド」が提案されています。Taskモナドは、計算を遅延評価し、明示的に実行を開始することができます。また、モナド法則を満たすため、計算の組み合わせやエラーハンドリングが予測可能になります。
Taskモナドの基本的な構造は以下のようになります。
// Taskモナドの例(疑似コード)
const Task = computation => ({
map: f => Task(resolve => computation(x => resolve(f(x)))),
chain: f => Task(resolve => computation(x => f(x).run(resolve))),
run: computation
});
このように、Taskモナドはmap
やchain
(またはflatMap
)メソッドを持ち、計算の組み合わせを可能にします。計算はrun
メソッドが呼ばれるまで実行されません。
まとめ
JavaScriptのPromiseは非同期処理を扱う上で非常に便利な機能ですが、モナド法則を完全には満たしていないため、厳密な意味でのモナドではありません。評価の即時性やエラーハンドリングの違いにより、モナドとしての性質を欠く部分があります。より厳密にモナドの性質を持つ構造を必要とする場合、Taskモナドなどの代替手段を検討することが有益です。これにより、関数型プログラミングのパラダイムに沿った、安全で予測可能な非同期処理が可能になります。