Promiseをカレー作りで喩える
数か月おきに、Promiseの役割(特に resolve
の意味)や細かい実行順がよくわからなくなってしまうため、いざというときに思い出しやすいよう、比喩的に内容をまとめておく。
主に自分向けなので、他の人にとって理解しやすいかどうかは保証しない。
小話
複数人でカレーを作っている場面を想定。
「ジャガイモの皮を剥き、一口大に切る」というタスクがあるとする。
シェルスクリプト風に書くなら...
cat "じゃがいも.txt" | 皮を剥く | 一口大に切る | ...
じゃがいもを取り出し、皮を剥き、剥き終わったらじゃがいもを一口大に切る、という一連の作業は、シェルスクリプトであれば |
(パイプ)を使って一連の処理として書ける。
シェルスクリプトは、sed や grep などのコマンドを|
(パイプ)でつなぐことで一連の処理を表現するが、この時の sed
や grep
のような1単位の処理を抽象化するものが Promise
であると考えるとわかりやすい。
Promiseで書くなら...
まず、Aさんに「ジャガイモの皮を剥いてください。完了したらあのカゴの中に入れてください。」と依頼したとする。
これをPromiseで書くとこのようになる。
const executor = (resolve) => {
const Promiseの結果 = 皮を剥く(じゃがいも)
resolve(Promiseの結果)
};
const promise = new Promise(executor);
用語との対応:
-
executor
=> Aさんへの依頼内容全体。ここでは、「ジャガイモの皮を剥いてください。完了したらこのカゴの中に入れてください」という依頼のこと。 -
Promiseの結果
=> 依頼内容の最終結果のこと。ここでは、皮を剥いたじゃがいも。 -
resolve
(resolutionFunc) => Promiseが成功した時に、Promiseの結果を入れる先。ここでは、カゴのこと。Aさんは依頼時にカゴの位置(=resolve)を引数として受け取り、Promiseの結果をカゴに入れる。
なお、Aさんに依頼した( new Promise()
)時点で、依頼内容(executor)の実行は開始される。
さて、材料の受け渡しに使っているカゴは、常に次の作業を行う人との境界線上に置かれている。今回は、Aさんがじゃがいもを入れたカゴは、Bさんの目の前にあるカゴだとする。
このとき、Bさんに「カゴの中にじゃがいもが置かれたら、それを一口大に切ってください」と依頼するとする。
これをPromiseで書くとこうなる。
function Bさんへの依頼内容(カゴの中身){
return 切る(カゴの中身)
}
p.then(Bさんへの依頼内容)
then
-> カゴの中身(Promiseの結果)を引数として受け取る、続いて行う処理を定義する。
Bさんは、依頼された時点ですでにカゴの中にじゃがいもがあれば、その場で一口大に切る作業を開始するし、まだじゃがいもが存在しない場合は、じゃがいもが置かれるまで待機し、じゃがいもがカゴに置かれたときから作業を開始する。
The eventual state of a pending promise can either be fulfilled with a value or rejected with a reason (error). When either of these options occur, the associated handlers queued up by a promise's then method are called. If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.
個人的には、resolve
はシェルスクリプトにおける |(パイプ) と同じく、手前の作業から次の作業へ値を橋渡しする役割、と考えると覚えやすい。
シェルスクリプトは常に標準入力と標準出力でデータを受け渡すと決まっているため、「処理の結果を置く場所」を次の処理に引き渡す必要がなく、そのためデータの受け渡しの記述がシンプル。
一方、javascriptはそうではないため、値の受け渡しのインターフェースとして、resolve
が必要になる。
なぜPromiseを使った記法がcallbackより優れているのか?
callbackを使った記法だと、Aさんに依頼する(=非同期処理を呼び出す)時点で、Bさんへの依頼内容(callback関数)も定義しておく必要がある。
一方、Promiseを使うことで、「非同期処理を行うメソッドにコールバック関数を渡す」という1段階の処理を、「非同期処理を行う関数はPromiseオブジェクトを返す」「返されたPromiseオブジェクトにthenでコールバック関数を渡す」という2段階の処理に分離でき、それによってコールバック関数を渡すタイミングの自由度が広がるのがメリット。
Promiseとは、AさんとBさんの間の値の受け渡しのインタフェースであり、「材料の受け渡し用のカゴ」のようなものだと考えやすい。
callbackの時は、Aさんに依頼する時点で、「皮を剥き終わった材料はBさんに渡してね」と伝える必要があった。
しかし、Promiseを用いる場合では、Aさんには「いったんカゴに入れておいて」と伝えるだけでよい。Aさんは、カゴに入れた後のことを知る必要がない。Bさんに対して「カゴに材料が入ったらそれを切っておいて」と伝えるタイミングは、いつでも構わない。
Promiseオブジェクトは、単なるcallbackと異なり、内部にPromiseの結果を保持することができる。これがカゴに相当する。
サンプルコード
前提として、Javascriptはシングルスレッドモデルのため、同期的な実行がすべて完了するまでは、非同期的実行は行われない。
console.log("1. カレー作り開始");
const Aさんへの依頼内容 = (resolve) => {
console.log("3. Aさんが作業開始");
setTimeout(() => {
console.log("6. Aさんがじゃがいもの皮剥きをしています");
resolve("剥けたじゃがいも");
}, 100);
};
function Bさんへの依頼内容(カゴの中身){
console.log("7. Bさんが作業開始");
setTimeout(() => {
console.log("8. Bさんがじゃがいもを切っています");
}, 100);
}
console.log("2. Aさんに依頼する");
const promise = new Promise(Aさんへの依頼内容);
console.log("4. Bさんに依頼する");
promise.then(Bさんへの依頼内容);
console.log("5. 依頼完了");
1. カレー作り開始
2. Aさんに依頼する
3. Aさんが作業開始
4. Bさんに依頼する
5. 依頼完了
6. Aさんがじゃがいもの皮剥きをしています
7. Bさんが作業開始
8. Bさんがじゃがいもを切っています
少し順序を入れ替えて、Bさんへ依頼するより先にカゴの中にじゃがいもが入っている場合でも、
Bさんがちゃんとじゃがいもを切ってくれることが確認できる。
console.log("1. カレー作り開始");
const Aさんへの依頼内容 = (resolve) => {
console.log("3. Aさんが作業開始");
console.log("4. Aさんのじゃがいもの皮剥き完了!");
resolve("剥けたじゃがいも");
};
function Bさんへの依頼内容(カゴの中身){
console.log("7. Bさんが作業開始");
setTimeout(() => {
console.log("8. Bさんがじゃがいもを切っています");
}, 100);
}
console.log("2. Aさんに依頼する");
const promise = new Promise(Aさんへの依頼内容);
console.log("5. Bさんに依頼する");
promise.then(Bさんへの依頼内容);
console.log("6. 依頼完了");
1. カレー作り開始
2. Aさんに依頼する
3. Aさんが作業開始
4. Aさんのじゃがいもの皮剥き完了!
5. Bさんに依頼する
6. 依頼完了
7. Bさんが作業開始
8. Bさんがじゃがいもを切っています
参考
こちらの本の第八章が非常に理解しやすかった。
こちらの記事も参考になった。
Discussion