今日までコールバックと戦ってきたみんなを、Promiseを信じた魔法少女を、私は泣かせたくない。
今日までコールバックと戦ってきたみんなを、Promiseを信じた魔法少女を、私は泣かせたくない。:
もう5年も前の記事になりますが、以下の記事がとても楽しげに
コールバック……駆逐してやる…この世から…一匹…残らず!!
上記の参考記事では
(※なぜか『魔法少女まどかマギカ』の一部ネタバレが含まれるので注意してください)
参考記事からの引用。コールバック地獄の原型です。
参考記事からの引用。
参考記事からの引用。
参考記事からの引用。(順次実行での比較なので
何かが解決されたような、されていないような。しかし悲しいかな
何かが解決されたような、されていないような。しかし悲しいかな
つまり「コールバック関数を引数に受ける1つ目の関数」「コールバック関数と1つ目の結果を引数に受ける2つ目の関数」という風に用意していく必要があります。
どういうことかというと、「2番目に関数を加えたい」とか「1番目の関数の結果を4番目の関数で使いたい」という変更(よくある)のときに悲劇が生まれます。
1. やりたいことを順番に(ネストすることなく)
2. メソッドチェーンで値を引きずり回さないこと。つまり値をメソッドチェーンの外側に持つこと。(※
それを念頭に入れて実直に行くと、こんな形になります。
最初の
一応、参考記事では型付けにこだわっていましたので、
重複している代入処理(
いっそのこと
スコープを気にする人ならカプセル化もしたいですよね。これは
このオブジェクト指向との相性の良さは、
参考記事で紹介されているように、
ここで行われていることは
もはやバイオリン奏者の怪我を治してあげたかったのか、彼と恋仲になりたかったのか、本当の気持ちが分からなくなりそうですね。JavaScript未経験の魔法少女に
最初の方に戻ってみましょう。
これの何がダメだったかというと…あ、あれ?なんだか…そこまで難しくない…?
そうだよね、
ちなみに
こういうものも大丈夫です。むしろ、こういうものに慣れた方がいい。魔法少女が銃火器で闘うくらい自然なことです。
hoge.txtとpiyo.txtを順次実行ではなく並列実行で読みたい場合(ほとんどはそうでしょう)は、順次実行用のコードを
常に
オブジェクト指向な感じに寄せると、メソッドチェーン部分はこんな風になります。
このように
コールバックはJavaScriptの欠点のように語られることもありますが、そんなことはないと思います。「コールバックが無いように見せかけたい」という願いが、形のない悪意となって、人間を内側から蝕んでゆくの、と誰かが言ってました。
Promiseちゃん、ありがとう。あなたは私の、最高の友達だったんだね。
おしまい
もう5年も前の記事になりますが、以下の記事がとても楽しげに
Promise
の厄介な側面を洗い出していて感動したので、アンサー記事的なものを書いてみたくなりました。コールバック……駆逐してやる…この世から…一匹…残らず!!
上記の参考記事では
Promise
系の実装としてjQuery.Deferred
を取り扱っていますが、本記事ではV8
の組込オブジェクトとしてのPromise
を取り扱います。(※なぜか『魔法少女まどかマギカ』の一部ネタバレが含まれるので注意してください)
目に焼き付けておきなさい。Promiseを使い間違えるって、そういうことよ。
参考記事からの引用。コールバック地獄の原型です。$.get("hoge.txt", function(hoge) { $.get("piyo.txt", function(piyo) { $.get("nyan.txt", function(nyan) { $.get("myon.txt", function(myon) { console.log(hoge + piyo + nyan + nyan); }); }); }); });
Promise
という魔法が生まれてすぐの段階です。var hoge; $.get("hoge.txt") .then(function(_hoge) { hoge = _hoge; return $.get("piyo.txt"); }) .then(function(piyo) { console.log(hoge + piyo); });
Promise
が暴走し、魔女化した状態です。$.get("hoge.txt") .then(function(hoge) { return $.get("piyo.txt").then(function(piyo) { console.log(hoge + piyo); });; });
asyncと契約して、魔法少女になってよ!
Promise
に息苦しさを覚えて、最初に手が伸びる先と言えばやはりasync
です。参考記事からの引用。(順次実行での比較なので
async.parallel
はasync.series
に書き換えています)function get(path) { return function(callback) { $.get(path).then(function(data) { callback(null, data); }); }; } async.series({ hoge: get("hoge.txt"), piyo: get("piyo.txt") }, function(err, results) { console.log(results.hoge + results.piyo); });
async.series
は「piyoに関する処理でhogeの結果を扱う」ということには不向きで、そういうときはasync.waterfall
を使うことになります。function get(path) { return function(callback) { $.get(path).then(function(hoge) { callback(null, hoge); }); }; } function get2(path) { return function(hoge, callback) { $.get(path).then(function(piyo) { callback(null, hoge, piyo + ' with ' + hoge); }); }; } async.waterfall([ get('hoge.txt'), get2('piyo.txt') ], function(err, hoge, piyo) { console.log({ hoge, piyo }); });
async.waterfall
に指定する関数は、前の関数の返却値を引数で受ける形式で実装する必要があります。つまり「コールバック関数を引数に受ける1つ目の関数」「コールバック関数と1つ目の結果を引数に受ける2つ目の関数」という風に用意していく必要があります。
どういうことかというと、「2番目に関数を加えたい」とか「1番目の関数の結果を4番目の関数で使いたい」という変更(よくある)のときに悲劇が生まれます。
Promiseが使いづらいなんて言われたら、私、そんなのは違うって。何度でもそう言い返せます。
Promise
を扱うコツは2つあります。1. やりたいことを順番に(ネストすることなく)
then
に入れてあげること。2. メソッドチェーンで値を引きずり回さないこと。つまり値をメソッドチェーンの外側に持つこと。(※
async
はそうなっていない)それを念頭に入れて実直に行くと、こんな形になります。
const results = {}; Promise.resolve() .then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge) .then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo) .then(() => console.log(results.hoge + results.piyo));
Promise.resolve()
はメソッドチェーン生成処理です。全ての関数をthen
に入れるためのおまじないとでも思ってください。一応、参考記事では型付けにこだわっていましたので、
then
が受けるFunction
の戻りを{Promise|any}
から{Promise}
に寄せたものも記載しておきます。一応。const results = {}; Promise.resolve() .then(() => $.get('hoge.txt')).then(hoge => Promise.resolve(results.hoge = hoge)) .then(() => $.get('piyo.txt')).then(piyo => Promise.resolve(results.piyo = piyo)) .then(() => Promise.resolve(console.log(results.hoge + results.piyo)));
results.hoge = hoge
とresults.piyo = piyo
)は一工夫できますが、可読性は上がりません。const results = {}; function acceptAs(name) { return o => results[name] = o; } Promise.resolve() .then(() => $.get('hoge.txt')).then(acceptAs('hoge')) .then(() => $.get('piyo.txt')).then(acceptAs('piyo')) .then(() => console.log(results.hoge + results.piyo));
then
から完全に無名関数を排除した方がシンプルです。こうするとPromise
が関数を呼び出していくメソッドチェーンにすぎないことがよく分かります。const results = {}; function getHoge() { return $.get('hoge.txt').then(hoge => results.hoge = hoge); } function getPiyo() { return $.get('piyo.txt').then(piyo => results.piyo = piyo); } function out() { console.log(results.hoge + results.piyo); } Promise.resolve().then(getHoge).then(getPiyo).then(out);
Promise
実装の1つの完成形だと思います。const Capsule = { getHoge: function() { return $.get('hoge.txt').then(hoge => Capsule.hoge = hoge); }, getPiyo: function() { return $.get('piyo.txt').then(piyo => Capsule.piyo = piyo); }, out: function() { console.log(Capsule.hoge + Capsule.piyo); } }; Promise.resolve().then(Capsule.getHoge).then(Capsule.getPiyo).then(Capsule.out);
Promise
を理解する上での勘所だと思います。なぜならPromise
には「関数を呼び出すが、状態は持たない」という特性があるからです。だから「状態を持った関数群」であるオブジェクトとは相性が良いのです。
訳が分からないよ。どうして人間はそんなに、同期的なコーディングスタイルにこだわるんだい?
参考記事で紹介されているように、co
とgenerator
でもかっこよく書けます。const co = require('co'); co(function*() { const hoge = yield $.get('hoge.txt'); const piyo = yield $.get('piyo.txt'); console.log(hoge + piyo); });
co
がgenerator
をnext
で進めてyield
で一旦generator
を抜けて返却されたPromise
がresolveされたらco
がその値をさらにnext
に渡してyield
を上書きすることで次のyield
へ進んで…もはやバイオリン奏者の怪我を治してあげたかったのか、彼と恋仲になりたかったのか、本当の気持ちが分からなくなりそうですね。JavaScript未経験の魔法少女に
co
を使ったソースコードを見せたら、ソウルジェムは一瞬でどす黒く濁るでしょう。
ほむらちゃん、ごめんね。私、Promiseの魔法少女になる。私、やっとわかったの。
最初の方に戻ってみましょう。const results = {}; Promise.resolve() .then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge) .then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo) .then(() => console.log(results.hoge + results.piyo));
そうだよね、
then
の中に処理を書いただけだもんね…。ちなみに
then
の中の関数はPromise
を返す必要はありません。const results = {}; Promise.resolve() .then(() => 'a' + 'b').then(hoge => results.hoge = hoge) .then(() => 'c' + 'd').then(piyo => results.piyo = piyo) .then(() => console.log(results.hoge + results.piyo)); // ⇒'abcd'
あるよ。順次実行も、並列実行も、あるんだよ。
hoge.txtとpiyo.txtを順次実行ではなく並列実行で読みたい場合(ほとんどはそうでしょう)は、順次実行用のコードをPromise.all([])
に入れます。const results = {}; Promise.all([ Promise.resolve().then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge), Promise.resolve().then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo) ]) .then(() => console.log(results.hoge + results.piyo));
Promise.resolve()
で始めてPromise.all([])
もthen
に入れるルールにすると、より強固に統一されたコーディングスタイルとなりますが、そこまでしなくていいでしょう。const results = {}; Promise.resolve() .then(() => Promise.all([ Promise.resolve().then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge), Promise.resolve().then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo) ])) .then(() => console.log(results.hoge + results.piyo));
Promise.all([ Promise.resolve().then(Capsule.getHoge), Promise.resolve().then(Capsule.getPiyo) ]).then(Capsule.out);
Promise
の真価は、順次実行と並列実行の同居のしやすさで発揮されます。(async
やgenerator
が順次実行と並列実行の同居を得意としていないだけのような気もします)const results = {}; Promise.all([ Promise.resolve($.get('hoge.txt').then(hoge => results.hoge = hoge)), Promise.resolve($.get('piyo.txt').then(piyo => results.piyo = piyo)) ]) .then(() => $.get('nyan.txt')).then(nyan => results.nyan = nyan) .then(() => console.log(results.hoge + results.piyo + results.nyan));
Promise
は順次実行と並列実行を縦横無尽に組み合わせることができ、実行順序についても一目で理解できます。もうちょっとコード量が減ったらいいなとは思いますが…。
これがコールバック。JavaScriptに選ばれた女の子が、契約によって生み出す宝石よ。
コールバックはJavaScriptの欠点のように語られることもありますが、そんなことはないと思います。「コールバックが無いように見せかけたい」という願いが、形のない悪意となって、人間を内側から蝕んでゆくの、と誰かが言ってました。ES6
やV8
が登場し、最初の魔法Promise
が、巡り巡ってコールバック地獄の歴代解決策に取って代わるとしたら…どこかで聞いたことのある話ですよね。(伏線回収できた?)Promiseちゃん、ありがとう。あなたは私の、最高の友達だったんだね。
おしまい
コメント
コメントを投稿