Javascript Promiseをわかりやすく”天使と悪魔”のアナロジーで理解する!絶対わかるPromise入門
こんにちは!
難しいインターネットやコンピューター技術をわかりやすく一風変わったアナロジーで解説します、でおなじみのしずかなかずしです。
JavaScriptやってますか?
今日は、みんな大好き! JavaScriptのES2015(ES6)バージョンで有名なPromiseに焦点を当てます。
Promiseというと、非同期の処理を扱いやすくするための機構ですが、その概念がちょっととっつきにくいと思われる初心者の方も多いのではないでしょうか。実は私もその一人でした。しかし、さまざまなプログラムをPromiseを使って書いていくうちに、ようやく人に説明できるほど十分に理解できたと思えるようになりました。
本日は、そんな私が「こういう風に理解すればいい」を、一風変わった「天使と悪魔」のアナロジーで解説していきます。
なぜPromiseは理解しにくいのか?
そもそも、Promiseは初心者にとってなぜに理解しにくいのでしょう?
その原因の1つは、世の中の解説の多くが、Promiseの文法や内部状態の説明など「どうやって使うか?」に焦点を当てたものが多いからです。確かに道具としてのPromiseを使いこなすためには、Promiseの仕様やプログラムの記述方法の理解は当然必要です。Promiseを使えば、JavaScript(というより、Nodejsのライブラリ)プログラミングで特に陥りやすい「コールバック地獄」といったコーディング上の問題を解決できる、という論点も納得感があります。
しかしながら、「どうやって使うか」というHowの理解だけでは、ちょっと枠から外れたシチュエーションで使えず、その都度、対面する状況に対する「どうやって使うか」をネットで調べることに時間を費やすことになります。
従って、この記事では、Promiseの書き方、を一旦置いといて、Promiseとは一体何なのか?というPromiseの世界観を「天使と悪魔」という独自のアナロジーで理解してみます。
ちなみに、当ブログしずかなかずしでは、以前にも一風変わったアナロジーで技術解説をしているものがあります。ホームネットワークを解説する「家」アナロジーです。よろしければ、こちらもあわせてご覧ください。
「天使と悪魔」アナロジーにおけるPromise
「天使と悪魔」アナロジーといっても、Promiseの解説に剛力 彩芽や渡部 篤郎が登場するわけではありませんので、あしからず。
ではまず、この「天使と悪魔」アナロジーにおいてPromiseとは何か?このアナロジーではPromiseは産まれたばかりの「赤ちゃん」と理解して下さい。しかも、ただの赤ちゃんではありません。将来天使か悪魔のどちらかに変容する、とてもこの世のものとは思えないような赤ちゃんを想像して下さい。
つまり、映画スターウォーズでいうところの、ルーク・スカイウォーカーみたいなものです。将来ライトサイドのジェダイの騎士(天使?)になるか、ダースベーダーのようにダークサイドの悪者(悪魔?)になるか、産まれたばかりではわからない。そんな感じです(アバウト…笑)。
という訳で、この後の解説ではスターウォーズ用語が多く出てくるので、映画スターウォーズをみたことがないとちょっときついかも知れませんが、お付き合い下さい。
運命を決めるのはプログラマー
では、誕生したスカイウォーカー(Promise)が、将来、ジェダイの騎士である天使になるのか、それとも悪魔のダースベーダー卿になるのかはどう決まるのでしょう?
その運命を決めるのは、プログラマーである「あなた」です。
スカイウォーカーであるPromiseに渡すプログラム(コード)こそが、スカイウォーカーの将来を決める重要な要素です。このコードが「暗黒面」を定義するといっても過言ではないでしょう(ちょっと何言ってるかわからない…)。
Promiseに渡すプログラムは、スカイウォーカーが大人になるまでいくら時間がかかってもよいです。しかし、プログラマーは最終的にスカイウォーカーを”resolve”と唱えて天使にするか、“reject"と唱えて悪魔にするのかを決める責任があります。
これを、ソースコードでみてみましょう。誕生したスカイウォーカーは、その時点では、天使でも悪魔でもありません。Promiseに渡したプログラマーが書いた関数将来を決めます。そのコードが実行され、遠い(?!)将来、resolve()が呼ばれたらスカイウォーカーは天使になります。もし、不慮の出来事により暗黒面を越えて reject()が呼ばれたら、スカイウォーカーは悪魔になります。
// スカイウォーカーの誕生
let skywalker = new Promise(function(resolve, reject) {
... resolve() // 天使(ジェダイの騎士)になる
... reject() // 悪魔(ダースベーダー)になる
});
Promiseの規格用語では、天使になったPromiseがfulfilledの状態になった、とよばれ、悪魔になったPromiseは、rejectedの状態になった、と呼ばれるわけです。産まれたばかりのスカイウォーカーは、Pending 状態とよばれます。
一般的なPromiseの説明で、いきなり状態の話をされても何のことかちんぷんかんぷんです。しかし、Promiseはまずは将来的にジェダイになるか、ダースベーダになるか作ったときにはわからないものという風に理解すると、わかりやすいのではないでしょうか。
天使から受け取る善(then)
成長して天使になったスカイウォーカーですが「善」の心を持った立派なジェダイの騎士になりました。騎士からライトセーバーを受け取ってみましょう。ジェダイになるためには時間がかかる、というのがポイントで、産まれたばかりのPromiseにgetLightSaber()と呼んだところで、すぐに結果が得られるものではありません。
そこで、天使になったとき、つまり「善」の心を持った時に初めてライトセーバーが受け取れるのです。その為にPromiseにはthen()(ゼン)という関数が用意されています。
成長したスカイウォーカーからライトセーバーを受け取ってみましょう。ポイントは、誕生した直後(new Promise)の時点では、skywalkerは天使なのか悪魔なのかわからないので、 lightSaver = skywalker.get()のような記述ができない、ということです。
let skywalker = new Promise(function(resolve, reject) {
...
resolve(theLightsaber) // 成長したスカイウォーカーはライトセーバーを持っている
...
})
skywalker.then(function(object) {
console.log(object + 'is theLightsaber')
})
悪魔から受け取る過ち(catch)
将来、運悪くスカイウォーカーがダークサイドに落ちてしまった場合はどうなるでしょう。
その場合は、「過ち」、すなわちエラーを受け取ることができます。その場合はPromiseのcatchでエラーを受けるとります。上記のコードを変更して、以下のようになるでしょう。
let skywalker = new Promise(function(resolve, reject) {
...
resolve(theLightsaber) // 成長したスカイウォーカーはライトセーバーを持っている
...
reject(new Error('ダークサイドに落ちました')
})
skywalker.then(function(object) {
console.log(object + 'is theLightsaber')
}).catch(function(error) {
console.log(error)
})
産まれたときから天使?!
ところで、世の中には奇妙な事も起こるもので、産まれたときから天使です、という人がいます。なんという幸運な人なのでしょう?
それを生み出すのはちょっと違った手順があります。
これを生み出すのが、Promise.resolve()というものです。解説書を読むと、この関数は「解決したPromiseを返します」とよくわからないことが書かれています。こう言われると、「は?どういうこと??」と思いませんか?私は思いました。
しかし、この関数は、産まれた時に既にフォースを習得しており、生まれながらにしてライトセーバーを操ることが出来るルーク・スカイウォーカーを生成するための関数なのです。
let luke = Promise.resolve(theLightsaber)
luke.then(function(lightsaber) {
console.log('this is the' + lightsaber)
})
複数世代に渡ってジェダイを作る?!
ここまでわかると、ジェダイの騎士を何世代にも渡って育てることができます。
よく非同期の処理を複数回ループさせたいという話が出てきます。とある非同期の処理(ジェダイを育てるのに時間がかかる)を1つ終わらせて、次の非同期処理を別のパラメータで実行、終わったらまた次…のような処理です。
そんなときも上の理屈がわかってればもう大丈夫。Promiseを使いこなせるはずです。
function reinforceLightsaber(lightsaber, level) {
return new Promise(function(resolve, reject) {
// levelを使ってlightsaberを強化する
lightsaber.level += level
resolve(lightsaber)
})
}
let forceLevels = [20, 100, 30, 80]
let lightsaber = {level:0}
let jedi = Promise.resolve(lightsaber)
forceLevels.forEach(function(level) {
jedi = jedi.then(function(lightsaber) {return reinforceLightsaber(lightsaber, level)})
})
jedi.then(function(theLightsaber) {
console.log('最後のジェダイのライトセーバーを受け取った! : level=' + theLightsaber.level)
})
概要を説明すると、1行目から7行目が、よくあるPromiseを返す関数です。
この関数では、前の世代のジェダイからlightsaberを受取り、その世代で強化するレベルlevelを使ってlightsaber.levelを強化(レベルの足し算)をします。要は、reinforceLightsaber()は
「自分の生涯をかけてlightsaberを磨く、最終的には天使になるようなスカイウォーカーを生成する関数」
です。生涯かけて…という処理は時間がかかる処理、つまり、すぐに終わらないような処理です。関数の戻り値は、将来天使になるであろうスカイウォーカーたる新しいPromiseです。非同期の実装をするときにはこういったPromiseをnewして戻り値にするような関数に非同期処理を書くのが一般的です。
この例では、9行目で各世代のジェダイがどれだけライトセーバーを強化するのかのレベルの配列が用意されています。
ループさせる際のポイントは、12行目でいきなり"天使になるPromise"を生成しているところです。いきなり天使になるので、引数に渡すlightsaberのlevelは0にしています(11行目)。そしてループの中で、jedi変数を毎回新しいジェダイで置き換えます。ここで、成長して天使になったら次のジェダイへライトセーバーを渡す処理をしている訳です。ライトセーバーは、非同期でジェダイが成長しおわらないと(resolveが呼ばれないと)次の世代に引き継げません。
全てのループが終わったら、17行目で各世代のジェダイが強化したライトセーバーを受け取る、という感じです。受け取るのはもちろん「善」の心then()関数ですね。
おわりに
Promiseってなに?
ネットでいろんな解説を読んでみて、わかったようなわからないような、という状況を解決すべく、JavaScriptのPromiseを「天使と悪魔」アナロジーで解説しました。
要は、Promiseは、最終的に天使になるか悪魔になるかわからない赤ちゃんのようなものなのです。その運命を決めるのはプログラマーであるあなたが書く関数です。これをPromiseに渡して生成するところから、赤ちゃんの人生が始まります。この関数の中で最終的に赤ちゃんを天使にするのはresolve()で、悪魔にするのは、reject()。
上の解説では、スターウォーズの壮大なストーリーの主人公をnew Promise()という得体の知れない物体に押し込んでみました。
何かのお役に立てれば!