コンセプト
- 実際に動かせる,日本語を使ったコードを例示する
- スガキヤのラーメンはうまい
Promise
って何?
ラーメンができたときに鳴るスガキヤの呼び出しベルです.
- ラーメンのお会計をする
- 呼び出しベルを受け取る
- この呼び出しベルは 客に出来上がったラーメンを渡す約束 (Promise) そのものを表す
- この約束があってはじめて客は**その後で (then) ラーメンを受け取る**という予定を立てることができる
- **約束の後に予定を生やす**というイメージ
- この後,ラーメンが準備できしだい,以下のイベントが実行される
- 店は客に出来上がったラーメンを渡す
- 客はラーメンを受け取る
いろいろなスガキヤと客
上でも書いたとおり
という日本語に読み替えられます.これを踏まえて,以下に示すコードを読みましょう.
一瞬でラーメンが完成するスガキヤ
ラーメンを注文して普通に食べます.console.log
関数を利用して,食べ終わったことを「Webブラウザのコンソール」あるいは「Node.jsを起動しているターミナル」に出力してみましょう.しかしこの例では,呼び出しベルを渡されたと思ったら席に戻る間も無く一瞬で回収されてしまいます.
function 食べる(食べ物) {
console.log('食べた: ' + 食べ物);
}
function お会計(商品名) {
console.log('お会計: ' + 商品名);
return new Promise(function (客を呼び出して渡す) {
console.log('できた: ' + 商品名);
客を呼び出して渡す(商品名);
});
}
お会計('ラーメン').then(function (渡されたもの) {
食べる(渡されたもの);
});
ラーメンが完成するまでに2秒かかるスガキヤ
ただ,実際はそんなに手際のよいスガキヤは存在しません.せいぜい3分ぐらいはかかるでしょう.しかし3分も待っていたら時間がもったいないので,ここでは最新の技術を駆使して2秒で作らせることにします.一定時間後にある処理を実行させるためにはsetTimeout
関数を使います.
function 試験勉強する(科目名) {
if (0 > 1) {
console.log('勉強した: ' + 科目名);
} else {
console.log('Twitterをした');
}
}
function 食べる(食べ物) {
console.log('食べた: ' + 食べ物);
}
function お会計(商品名) {
console.log('お会計: ' + 商品名);
return new Promise(function (客を呼び出して渡す) {
setTimeout(function () {
console.log('できた: ' + 商品名);
客を呼び出して渡す(商品名);
}, 2000);
});
}
お会計('ラーメン').then(function (渡されたもの) {
食べる(渡されたもの);
});
試験勉強する('英語');
呼び出しベルを使っているので,どれだけかかろうがなんだろうが,レジの前で延々と待たされることはありません.準備が出来次第いい感じに呼び出してくれるので,その間の時間をたった2秒でも有効活用することができます.
ラーメンの後にソフトクリームを貪るデブ
また,ソフトクリームをラーメンと一緒に注文すると溶けてしまうので,ラーメンを食べ終わってからソフトクリームを注文するという処理も可能です.更にソフトクリームを食べ終わったらもう1回おかわりするとしましょう.
お会計('ラーメン').then(function (渡されたもの) {
食べる(渡されたもの);
お会計('ソフトクリーム').then(function (渡されたもの) {
食べる(渡されたもの);
お会計('ソフトクリーム(おかわり)').then(function (渡されたもの) {
食べる(渡されたもの);
});
});
});
これは,以下のように書くこともできます.こちらのほうがコードが縦に続く感じで読みやすいんじゃないでしょうか?
お会計('ラーメン').then(function (渡されたもの) {
食べる(渡されたもの);
return お会計('ソフトクリーム');
}).then(function (渡されたもの) {
食べる(渡されたもの);
return お会計('ソフトクリーム(おかわり)');
}).then(function (渡されたもの) {
食べる(渡されたもの);
});
「その後で」の中で新たな約束をreturn
して次に繋げることができるんです!
(番外編) ムーディ勝山 その1
目的がパッと思いつきませんが,今後のための説明も兼ねて,ただ右からきたものを左へ受け流すだけのPromise
を使った処理を書いてみます.
new Promise(function (左へ受け流す) {
左へ受け流す('何か');
})
.then(function (右からきたもの) {
return new Promise(function (左へ受け流す) { 左へ受け流す(右からきたもの); });
})
.then(function (右からきたもの) {
return new Promise(function (左へ受け流す) { 左へ受け流す(右からきたもの); });
})
.then(function (右からきたもの) {
console.log('右から 右から ' + 右からきたもの + 'がきてる');
});
**「形式的にPromise
を使うが,すぐに受け流すだけで時間のかかる処理をしない」**というときのために,より簡単な書き方が用意されています.Promise.resolve
という関数を使うパターン,そしてそれすら省略できてしまうパターンがあります.上に書いたものは下に書いたものと完全に等価です.
Promise.resolve('何か')
.then(function (右からきたもの) {
return Promise.resolve(右からきたもの);
})
.then(function (右からきたもの) {
return Promise.resolve(右からきたもの);
})
.then(function (右からきたもの) {
console.log('右から 右から ' + 右からきたもの + 'がきてる');
});
Promise.resolve('何か')
.then(function (右からきたもの) {
return 右からきたもの;
})
.then(function (右からきたもの) {
return 右からきたもの;
})
.then(function (右からきたもの) {
console.log('右から 右から ' + 右からきたもの + 'がきてる');
});
return
を書かないと何も受け流せませんが,then
の返り値は常にPromise
であることが保証されているので,以下のように書いても最後のconsole.log
は実行されます.ただし,undefined
が受け流されたことになってしまいます.
Promise.resolve('何か')
.then(function (右からきたもの) {
})
.then(function (右からきたもの) {
})
.then(function (右からきたもの) {
console.log('右から 右から ' + 右からきたもの + 'がきてる');
});
ラーメンとソフトクリームを別々に注文した挙句後から**「2つともできたときに一緒に渡して!」**とか言ってくるめんどくさい客
そんな面倒くさい注文に対応する方法もあります.Promise.all
という関数を使います.これは
- 呼び出しベルの配列をまとめて受け付ける
- 全部が準備できるまで待つ
- 呼び出しベルの配列を食べ物の配列に入れ替えて客に渡す
という動きをします.
Promise.all([
お会計('ラーメン'),
お会計('ソフトクリーム'),
]).then(function (渡されたもの一覧) {
console.log('両方一気にいただきまーす');
console.log(渡されたもの一覧);
});
注文を受理して2秒後に品切れに気づいたスガキヤ店員
さて,ここまで順調にラーメンを提供できていましたが,半額キャンペーンなんかやってる日には品切れになってしまうこともあります.店員が後で気づいたとき,どうすればいいでしょうか?
ここまで,new Promise(...)
の部分に書いている関数は,**「客を呼び出して渡す」という引数しかとっていませんでした.実はもう1つ「客を呼び出して言い訳する」**という引数をとることができるんです!
function 食べる(食べ物) {
console.log('食べた: ' + 食べ物);
}
function お会計(商品名) {
console.log('お会計: ' + 商品名);
return new Promise(function (客を呼び出して渡す, 客を呼び出して言い訳する) {
setTimeout(function () {
console.log('やばい!品切れ: ' + 商品名);
客を呼び出して言い訳する(new Error(商品名 + 'は品切れでした…'));
}, 2000);
});
}
では客の方はどうでしょうか?そのままでは,注文が失敗したときには「シーン」として何も起こりません.店員の言い訳を聞くためには,以下のどちらかの書き方をする必要があります.
お会計('ラーメン').then(
function (渡されたもの) {
食べる(渡されたもの);
},
function (言い訳) {
console.error('店員の言い訳: ' + 言い訳.message);
}
);
お会計('ラーメン')
.then(function (渡されたもの) {
食べる(渡されたもの);
})
.catch(function (言い訳) {
console.error('店員の言い訳: ' + 言い訳.message);
});
catch
を使う方法は,以下と等価です.
お会計('ラーメン')
.then(function (渡されたもの) {
食べる(渡されたもの);
})
.then(undefined, function (言い訳) {
console.error('店員の言い訳: ' + 言い訳.message);
});
但し,このcatch
メソッドはtry~catch構文で用いられる**catch
キーワード**とは無関係なので注意してください.
try {
throw new Error('ラーメンは品切れでした…');
} catch (言い訳) {
console.error('店員の言い訳: ' + 言い訳.message);
}
(番外編) ムーディ勝山 その2
ムーディ勝山は,正しく実行された結果をreturn
して左へ受け流さないと次に繋がりませんでした.では失敗した結果に関してはどうでしょうか?
new Promise(function (左へ受け流す, 左奥へぶっ飛ばす) {
左奥へぶっ飛ばす('何か');
})
.then(function (右からきたもの) {
})
.then(function (右からきたもの) {
})
.then(
function (右からきたもの) {
console.log('右から 右から ' + 右からきたもの + 'がきてる');
},
function (右奥からぶっ飛んできたもの) {
console.error('右奥から 右奥から ' + 右奥からぶっ飛んできたもの + 'がぶっ飛んできた');
}
);
無事,「何か」が飛んできたと思います.失敗時の処理の流れは以下のようになります.
-
then
の繋がりを伝って,最初に「右奥から」として処理される場所に流れ着く
このように失敗は伝播していくので,先ほどの catch
のような書き方ができるわけです.また,もう1点の補足として,Promise.resolve
と対を為す関数としてPromise.reject
を紹介しておきます.
new Promise(function (左へ受け流す, 左奥へぶっ飛ばす) {
左奥へぶっ飛ばす('何か');
})
.catch(function (右奥からきたもの) {
return new Promise(function (左へ受け流す, 左奥へぶっ飛ばす) { 左奥へぶっ飛ばす(右奥からきたもの); });
})
.catch(function (右奥からきたもの) {
return new Promise(function (左へ受け流す, 左奥へぶっ飛ばす) { 左奥へぶっ飛ばす(右奥からきたもの); });
})
.catch(function (右奥からきたもの) {
console.log('右奥から 右奥から ' + 右奥からきたもの + 'がぶっ飛んできた');
});
上に書いたものは下に書いたものと等価です.但し,throw
に関してはどこでも認められるわけではないということを明記しておきます.これについては,余力のある人向けに最後に説明を入れます.
Promise.reject('何か')
.catch(function (右奥からぶっ飛んできたもの) {
return Promise.reject(右奥からぶっ飛んできたもの);
})
.catch(function (右奥からぶっ飛んできたもの) {
return Promise.reject(右奥からぶっ飛んできたもの);
})
.catch(function (右奥からぶっ飛んできたもの) {
console.log('右奥から 右奥から ' + 右からぶっ飛んできたきたもの + 'がぶっ飛んできた');
});
Promise.reject('何か')
.catch(function (右奥からぶっ飛んできたもの) {
throw 右奥からぶっ飛んできたもの;
})
.catch(function (右奥からぶっ飛んできたもの) {
throw 右奥からぶっ飛んできたもの;
})
.catch(function (右奥からぶっ飛んできたもの) {
console.log('右奥から 右奥から ' + 右からぶっ飛んできたきたもの + 'がぶっ飛んできた');
});
本気でPromise
を理解したい人向けの説明
setTimeout
の有無による実行タイミングの違い(?) [修正済み]
実は,ここまでで,ある重大なことを1つ誤魔化しています.これを最初から説明していると初心者門前払いになってしまうので,敢えて間違った説明をしてきたのですが,気になる方・余力のある方は是非ここを読んでください.
一番最初の「一瞬でラーメンが完成するスガキヤ」の例について考察しましょう.
function 食べる(食べ物) {
console.log('食べた: ' + 食べ物);
}
function お会計(商品名) {
console.log('お会計: ' + 商品名);
return new Promise(function (客を呼び出して渡す) {
console.log('できた: ' + 商品名);
客を呼び出して渡す(商品名);
});
}
お会計('ラーメン').then(function (渡されたもの) {
食べる(渡されたもの);
});
半ば強引に進めてきたと思いますが,本気で理解しようとすると,以下のような疑問が自然とわくはずです.
Promise
を作るときに渡してる関数は何?
function (客を呼び出して渡す) {
console.log('できた: ' + 商品名);
客を呼び出して渡す(商品名);
}
これはPromise
側によって**直ちに**実行されます.いったい何故このコードは関数で書く必要があるのでしょうか?それは, 客を呼び出して渡す
という引数を受け取るためです.
客を呼び出して渡す
はどこからきてるの?
function (渡されたもの) {
食べる(渡されたもの);
}
この関数がまるごと引数 客を呼び出して渡す
に代入され,A
がB
を呼び出す形になります…と,言いたいのですが,実際は違います.客を呼び出して渡す
の正体は,Promise
側で適当に用意してくれる関数です.
なぜこんなことをするか?理由の1つは「お会計」を定義する側からは,then
が後ろに続いているかどうかを知るすべはないからです. 後ろにthen
がある場合とない場合でif分岐を書かせるのも最悪なので,この理由は納得できると思います.
実行開始: お会計('ラーメン')
実行開始: new Promise(A)
実行開始: A(客を呼び出して渡す)
実行開始: 客を呼び出して渡す(商品名) … Promiseが用意した仮の関数
実行終了: 客を呼び出して渡す(商品名) … 渡された「商品名」を覚えておく (もし後ろにthenが無ければこいつの努力は無駄になる)
実行終了: A(客を呼び出して渡す)
実行終了: new Promise(A)
実行終了: お会計('ラーメン')
実行開始: .then(B)
実行終了: .then(B)
-------- 0ミリ秒の間 (setTimeoutで0ミリ秒待つよりも更に極めて短い遅延) --------
実行開始: ☆thenで予約された処理を実際に呼び出す関数☆
実行開始: B(渡されたもの) … ここで覚えておいた「商品名」を「渡されたもの」として流す
実行開始: 食べる(渡されたもの)
実行終了: 食べる(渡されたもの)
実行終了: B(渡されたもの)
実行終了: ☆thenで予約された処理を実際に呼び出す関数☆
比較のために,setTimeout
ありの場合も入れてみます.
実行開始: お会計('ラーメン')
実行開始: new Promise(A)
実行開始: A(客を呼び出して渡す)
実行開始: setTimeout(★遅延実行させる関数★, 2000)
実行終了: setTimeout(★遅延実行させる関数★, 2000) … 予約が終わっただけで中身は実行されていない
実行終了: A(客を呼び出して渡す)
実行終了: new Promise(A)
実行終了: お会計('ラーメン')
実行開始: .then(B)
実行終了: .then(B)
-------- 2000ミリ秒の間 --------
実行開始: ★遅延実行させる関数★()
実行開始: 客を呼び出して渡す(商品名) … Promiseが用意した仮の関数
実行終了: 客を呼び出して渡す(商品名) … 渡された「商品名」を覚えておく (もし後ろにthenが無ければこいつの努力は無駄になる)
実行終了: ★遅延実行させる関数★()
-------- 0ミリ秒の間 (setTimeoutで0ミリ秒待つよりも更に極めて短い遅延) --------
実行開始: ☆thenで予約された処理を実際に呼び出す関数☆
実行開始: B(渡されたもの) … ここで覚えておいた「商品名」を「渡されたもの」として流す
実行開始: 食べる(渡されたもの)
実行終了: 食べる(渡されたもの)
実行終了: B(渡されたもの)
実行終了: ☆thenで予約された処理を実際に呼び出す関数☆
then
に渡したB
は,then
で予約された処理を実際に呼び出す関数によって後から呼び出されることが決まっているため,setTimeout
の有無に関わらず,実行順序は保証されています.
throw
が認められるとき,認められないとき
どうやって説明しようか苦悶していたのですが,これを正しく説明するためにはPromise
のインスタンスが持つメソッド,および先程も登場したthen
で予約された処理を実際に呼び出す関数についての特徴を明らかにする必要があります.
分かりやすいように,JavaScriptの標準規格である ECMAScript 2015 (ES2015, ES6) のクラス構文に近い擬似コードとしました.
class Promise
{
constructor(A)
{
try {
A(「客を呼び出して渡す」の正体, 「客を呼び出して言い訳する」の正体);
} catch (言い訳) {
「客を呼び出して言い訳する」の正体(言い訳);
}
}
thenで予約された処理を実際に呼び出す関数()
{
try {
...
} catch () {
...
}
}
then(B) { ... }
catch(B) { ... }
}
要点は以下の通りです.
- コンストラクタ実行中に
throw
された言い訳は自動的にcatch
され,客を呼び出して言い訳する
が実行される. -
then
で予約された処理を実際に呼び出す関数についても,throw
されたものをcatch
してくれる点では共通.
要するに…
実行開始: new Promise(...)
実行終了: new Promise(...)
実行開始: ☆thenで予約された処理を実際に呼び出す関数☆
実行終了: ☆thenで予約された処理を実際に呼び出す関数☆
このいずれかに挟まれている場所なら大丈夫だということになります.それでは,ダメなケースの具体例を考えてみましょう.
new Promise(function (客を呼び出して渡す, 客を呼び出して言い訳する) {
setTimeout(function () {
console.log('やばい!品切れ: ' + 商品名);
throw new Error(商品名 + 'は品切れでした…');
}, 2000);
});
もしこれを呼び出すと,throw
はどこに入るでしょうか?
実行開始: new Promise(A)
実行開始: A(客を呼び出して渡す, 客を呼び出して言い訳する)
実行開始: setTimeout(★遅延実行させる関数★, 2000)
実行終了: setTimeout(★遅延実行させる関数★, 2000) … 予約が終わっただけで中身は実行されていない
実行終了: A(客を呼び出して渡す, 客を呼び出して言い訳する)
実行終了: new Promise(A)
-------- 2000ミリ秒の間 --------
実行開始: ★遅延実行させる関数★()
throw
もうこれでお分かりですね.