この記事はちょっと愚痴っぽいです。 ←Promiseが分かりにくいと思った編集者のメモ書きなので、内容が不正確な場合があります。 |
JavaScript(ECMAScript)におけるPromiseとは、非同期処理の約束された未来成功または失敗を表すオブジェクトである。
といわれてもよくわからなかった編集者が、悩んだ点について、にわか仕込みの知識で述べる。分かっている人には当たり前のことかもしれないが、分かっていない人にとって非直感的で躓くのではないかというポイントを重点的に述べるつもりなので、通常とは説明の仕方がずれていたとしてもそれは仕様です。
通常プログラムは上から順番(同期的: synchronous)に実行される。しかし非同期(: asynchronous)処理は上から順番に実行されるとは限らない。
これを無理やり制御しようとするとCallback Hellと呼ばれる深いネストが出来上がるが、ここでは触れない。
Promiseは非同期処理を実現するオブジェクトと紹介されるが、実際には非同期処理を実現するのはsetTimeout()のような処理であって、Promiseは実行順序の制御をしているに過ぎない。
オブジェクトインスタンスの生成と言うと、コンストラクタが定番だが、Promiseは
などにより生成される。コンストラクタによる生成が、thenによる生成と挙動が異なることが、初見では非直感的に思えるかもしれない。
たいていの説明ではPromiseの内部の時間軸に焦点をあてて解説するのだが、そのせいで外部との関係が曖昧になっているように思う。
↓前の処理
↓→Promiseの処理→
↓後の処理
Promiseのコンストラクタの説明に、new Promise()は内部の関数がresolve()されるまでPromiseの値が返らないというような説明をしていることがあるが、以下で述べる.then()の引数になる処理が呼ばれないという意味であり、new Promise()の値が返ってから「後の処理」が行われるという意味ではない。
new Promise()の内部の関数処理は別の世界線時間軸に移動し、「後の処理」はPromiseの処理の進行状況とは無関係に進んでいく。
たいていの解説ではここでコンストラクタを出してくるが、Promiseのコンストラクタは、2つの関数を引数に取る関数を引数に取る関数という禍々しいものであるため、後回しにする。例外処理もできるのだが、本質でないので触れない。
Promise.resolve(初期値)
.then(function(初期値を受ける引数){処理1})
.then(function(処理1の戻り値を受ける引数){処理2})
.then(function(処理2の戻り値を受ける引数){処理3});
処理1、処理2、処理3が順番に実行されることが保証される。
このthen()というメソッドは、引数になる関数がPromiseを返す関数かどうかで挙動が変わるという気持ち悪い動的型付け言語ならではの挙動になっている。本来2種類のメソッドであるべきところを動的型付けであるのをいいことに無理矢理1本化していることが、Promise.then()をさらにわかりにくくしているように思う。
また、コンストラクタとはとる引数の種類も違う。本当はエラー処理させる関数も含めて2つ引数を取ることができるが、本質でないので触れない。
引数となる関数の返す値がPromiseでない場合は、コンストラクタではresolveが呼ばれるまで待機させる方法があるが、then()自体は終了を待機させる手段を持たない。
引数となる関数の返す値がPromiseの場合は、返す値のPromiseが待機状態の間は、then()の戻り値であるPromiseも待機状態となる。
then()が返すPromiseオブジェクトは、then()を呼び出したPromiseとは別のインスタンスである。
then()の後に何種類かの処理を同時にスタートさせたい場面を考えれば、必然的な実装であると言える。同時にスタートさせた処理の両方の値が揃うのを待つ、all()というメソッドがある。ちなみに、いずれかの処理が終わるのを待つメソッドはrace()である。
const p0 = Promise.resolve();
const p1 = p1.then(処理1);
const p2 = p1.then(処理2);
Promise.all([p1, p2]).then(処理1と処理2の結果を必要とする処理);
Promiseのインスタンスは内部に.thenの引数となる関数に渡されるべき値を持っているが、この値をPromiseの外部で使う手段は提供されていない。値を使いたければ、then()の引数に値を使う関数を入れるしか無い。
内部で外部の変数に代入すれば持ち出せることは持ち出せるが、Promiseの内部と外部で時間の流れを同期させる方法がないため推奨されない?
の3つの状態を取りうるが、コード上にはそれが反映されない。
Promiseのコンストラクタは、2つの関数を引数に取る関数を引数に取る関数という禍々しい高階関数であるが、あまり問題視されないのは、言語仕様だから仕方ないPromiseは通常コンストラクタで自作するものではなく、他のライブラリ関数から戻り値として渡されるものだかららしい。
new Promise(function(resolve, reject) {
resolve()を呼ぶ処理
});
----------------------------------------------------
// 説明のために以下のように分けて書く。
const executor = function(resolve, reject) {
resolve()を呼ぶ処理
};
new Promise(executor);
といったあたりだろうか(多すぎ)。
これでは実行順序の制御が出来ないかのように見えるが、以下のようにするとPromiseの外部との間に順序をつけることができる。
const promise1 = new Promise(略);
const promise2 = promise1.then(function(引数){処理1});
処理2
promise2.then(function(引数){処理3});
処理4
このやりかたが公式に推奨されるかどうかは知らない。
ただ、これで処理2が処理3より前に実行されることだけは保証される。
ただし、処理3と処理4の前後関係はthen()の内部はPromiseの時間軸で処理が進んでいくため、やはり前後関係が保証されない。
処理1が終わっていなくてもpromise2に処理3が追加されて処理4に進んでいくため、処理1が処理4よりも先に終了する保証もない。
Promiseはモナドになっているのだが、あまりそのことについて考えてもメリットはない。モナドについて調べたことがあるのでせっかくだから書いておくが、上述したこととも一部重複しているし、モナドを知らないのならここは読み飛ばしたほうが身のためである。
unit()の関数の性質についてはモナド(プログラミング)を参照のこと。
多くのモナドではコンストラクタが、unit()に相当することが多いが、Promiseではコンストラクタはunit()ではない。Promiseが非直感的である理由の一つではないかと思う。
unit()に相当するのは(基本的には)Promise.resolve()である。
Promise.reject()もunit()に相当するが、後述のように、これはPromiseが処理が失敗した場合という、通常の値では表現できない状態まで表現対象に含めていることに起因する。
flatMap()の関数の性質についてはモナド(プログラミング)を参照のこと。
flatMap()に相当するのがthen()なのだが、then()が引数に取る関数が、flatMap()は一つだけなのに対し、then()はthen()呼ぶPromiseが成功していた場合と失敗していた場合2つの関数を引数に取る。一つの関数が内部でifを使って分岐していると思えば一つの関数かもしれない。
しかもthen()は、引数となる関数の戻り値がPromiseであるかどうかによってflatMap()になったりmap()になったりと挙動不審である。
失敗を扱うという点がMaybeモナドに似たところがなくもない。
Promiseの内部で行った計算結果はPromiseの外には持ち出せない。値を取り出せるモナドも多いが、モナドでは本来値を取り出せることは保証されないものである。
モナドに意味を求めるべきかどうかという問題はあるが、モナドの性質という点から捉えると、
ということだろうか
モナドと言えばHaskellだが、Haskellではこの種類のモナドはあまり使用されない。
Haskellはデフォルトで遅延評価な言語なので、もともと実行順序が制御できない。このような方法で実行順序を制御する必要はないのでPromiseに相当するモナドは重視されない。それでも実行順序の制御が必要な場合はdo記法がある。
掲示板
4 ななしのよっしん
2021/06/09(水) 08:27:46 ID: K1QNFPkiUt
>>1
PromiseもしくはPromiseパターンという項を作るのがよいのかねぇ。
5 ななしのよっしん
2022/10/13(木) 16:53:47 ID: ytcOp4Myth
難解だよなこれ
何故大百科に記事がwとは思ったが
まあたまに変な方向に詳しい記事あるのも大百科の面白いとこではある
6 ななしのよっしん
2022/10/13(木) 16:56:29 ID: lJfR2UzIEq
async/awaitサポートが最近見るけどそもそもモナド構文がサポートされている言語だとわざわざasync/await構文をサポートする意味もないよね
急上昇ワード改
最終更新:2024/04/20(土) 10:00
最終更新:2024/04/20(土) 10:00
ウォッチリストに追加しました!
すでにウォッチリストに
入っています。
追加に失敗しました。
ほめた!
ほめるを取消しました。
ほめるに失敗しました。
ほめるの取消しに失敗しました。