最近はNode.jsで簡単なスクリプトを実行することが多く、その時に改めてasync/awaitわかりやすいなーと感じることがあったのでそれについてつらつら記述していきます。
- 手っ取り早く結果を知りたい人は以下の部分だけ読むと良いと思います。
- 指摘やアドバイスありましたら是非コメントをお願いします。必要であれば内容も修正していきます。
何をしたいか
私がやっていた作業は、「データベースのデータを全て抜き出す」というものでした。
普通に全てのレコードを取得しようとすると、DBサーバーが"too large"とエラーを返すので、
少しずつ取得してスクリプト側でマージしていく必要がありました。
「取得結果が0件になるまで繰り返しレコードを取得」という処理をしたいわけです。(イメージを下に書いておきます)
let result;
let from = 0;
const count = 1000;
while (true) {
from += count;
result = record_shutoku(from, count); // record_shutoku()は単純にレコードの配列を返してくれると想定
if (result.length == 0) {
break;
}
}
何が問題なのか
record_shutoku()
みたいな重い処理はJSでは大抵非同期実行になります。今回はこれがPromise
を返す関数だと想定します。
何回繰り返すのかがわかっていないというのが今回の問題で、
繰り返す回数がわかっていて、並列実行して構わないのであればPromise.all()
を使えばOKです。
(今回は深い事情により事前にレコードの件数が取得できず、しかも並列実行もできない状況でした。お金があれば解決できたかもしれません)
また非同期実行関数(Promise
を返す関数)の同期実行版が用意されているのであれば、while
とそれを使えば解決です。(上で書いたイメージがまさにそれです)
けど普通は用意されてないことの方が多いと思います。
- 用意してる例:
fs.readFile()
とfs.readFileSync()
- 用意してない例:
fetch()
用意してないこと自体は全く問題ないです。むしろ正常な姿だと思います。
さて非同期実行関数を同期的に繰り返すにはどうしたら良いものか。
async/awaitが使える場合
async/awaitがあるとあっさり解決
Node.js 8以降を使っていれば簡単に解決する。
record_shutoku()
がPromise
を返す関数と想定すると、
async function main() {
let result;
let from = 0;
const count = 1000;
while (true) {
from += count;
result = await record_shutoku(from, count); // record_shutoku()はPromiseを返してくれると想定
if (result.length == 0) {
break;
}
}
}
main(); // 実行
このように書ける。処理全体をasync
関数として作って、await
をrecord_shutoku()
の前につけるだけで良い。
もしエラー処理をしたければrecord_shutoku()
をtry/catchしてあげるだけで良い。
確認のために実行できる例
私の場合は脆弱だったHigh Sierra上で、asdfとNode.js 8.9.1を使って実行を確認しています。
// 受け取った値 + 1をPromiseで返すだけの関数
// for文は最初に実行されるcalcほど処理を重くするためのもので、本当に同期実行されてるか確認するためのものです。
function calc(num) {
return new Promise((resolve, reject) => {
for (let i = 1; i <= 10 - num; i++) {
console.log(` calc(${num}): ${i}`);
}
return resolve(num + 1);
});
}
async function main() {
console.log('start');
let num = 0;
while (true) {
num = await calc(num);
console.log('main(): result is ' + num); // ここは同期実行できてるか確認するためのものです。
if (num >= 10) {
break;
}
}
console.log('done');
}
main();
実行すると以下のような出力がされて、同期的に実行できてることがわかる。
start
calc(0): 1
calc(0): 2
calc(0): 3
calc(0): 4
calc(0): 5
calc(0): 6
calc(0): 7
calc(0): 8
calc(0): 9
calc(0): 10
main(): result is 1
calc(1): 1
calc(1): 2
calc(1): 3
calc(1): 4
calc(1): 5
calc(1): 6
calc(1): 7
calc(1): 8
calc(1): 9
main(): result is 2
... # 省略
main(): result is 9
calc(9): 1
main(): result is 10
done
まとめ1
- async/awaitありがたい
- 非同期処理の関数を同期的に繰り返すのも簡単
- try/catchも普通に使える
async/awaitが使えない場合どうすればいいですか?
今回の私の状況はまさにそれで、Node.js 6を使わなければいけない事態でした。(お金と時間があればNode.js 8にアップデートできたかもしれません)
したがってasync/awaitが使えません。
とりあえず軽くやり方を探ってみた
検索してみて、見つかったのは以下でした。
- https://stackoverflow.com/questions/29880715/how-to-synchronize-a-sequence-of-promises
- https://stackoverflow.com/questions/28683071/how-do-you-synchronously-resolve-a-chain-of-es6-promises
一番シンプルと思われるものを抜粋
var p = Promise.resolve();
for (let i=1; i<=10; i++) {
p = p.then(() => promiseReturner(i));
}
ただ上記のやり方だと繰り返す回数がわかっているという前提が必要なのと、async/awaitでやっているbreak
が実現できない。
どうすればできるか
以下のように再帰関数を使うと実現できます。(実は独力で解決したわけではなく、弊社のハイパーエンジニアに助けていただきました)
// calc()自体はasync/awaitの例と全く同じ
function calc(num) {
return new Promise((resolve, reject) => {
for (let i = 1; i <= 10 - num; i++) {
console.log(` calc(${num}): ${i}`);
}
return resolve(num + 1);
});
}
function kurikaeshi_calc(num) {
if (num >= 10) {
return Promise.resolve(num);
} else {
return calc(num).then((n) => {
console.log('kurikaeshi_calc(): result is ' + n);
return kurikaeshi_calc(n);
});
}
}
console.log('start');
kurikaeshi_calc(0).then(num => {
console.log('done');
});
出力はasync/awaitのものと同じなので省略。
少しだけ解説
処理の本流は
-
console.log('start')
で文字列出力 -
kurikaeshi_calc(0)
がcalc(0).then(...)
でできるPromise
を返す - それに対して
.then(num => console.log('done'))
で次の処理を登録
これで終了。
問題はPromise
の中での処理。
-
calc(0).then(...)
の...
でkurikaeshi_calc(1)
、すなわちcalc(1).then(...)
を返す -
calc(1).then(...)
の...
でkurikaeshi_calc(2)
、すなわちcalc(2).then(...)
を返す - (省略)
-
calc(9).then(...)
の...
でkurikaeshi_calc(10)
、すなわちPromise.resolve(10)
を返す
上記のように最後以外は .then()
によって次の処理を登録済みのPromise
を返してくるので、
それら全部の実行が終わるまで.then(num => console.log('done'))
は待っていることになる。
まとめ2
- async/awaitありがたい
- 再帰関数でも実はそれなりにシンプルに書ける
- 関数型プログラミング思考は大事だと再認識
- エラー処理は普通に
.catch()
すればOK - とはいえ(人によっては)ややこしく思える
Promise
が使えない場合どうすればいいですか?
頑張ってください。
すいません。特に必要に迫られてないので考えてないです。
たぶんPromise
の例とそんなに変わらないと思ってます。
総括
今回はES7のasync/awaitの便利さを感じるとともに、自分の関数型プログラミング力の甘さを再認識するテーマとなりました。
ただ元も子もないことを言えば、適当なスクリプト書く程度であればそもそも無理してNode.jsを使わなくてもいいんですけどね。
明日は @magicant さんの「cd コマンドの罠」です。罠とはなんだろう...気になる!