LoginSignup
1
1

JS の for await...of の使いどころ

Last updated at Posted at 2023-12-18

for await...of

こういう書き方ご存知でしょうか。

for await (const item of asyncIterable) {
  ...
}

for の直後に await が書かれています。ECMAScriptの正式な記法ですが、使わなければいけないタイミングが少なくあまり認知されていない気がします。自分は最近知りました。

端的に言うとPromiseを直列にループしたい時の記法です。

ただ、重要なのは AsyncIterable を扱う場合であり、シンプルなPromiseの配列を回す時は通常の for...of の中で await する方法でも特に大きな問題はありません。

結論

使う必要があるタイミングは

  • AsyncIterable なオブジェクトを回す時

使っても良いタイミングは

  • Promiseの配列を直列に回したい時

使ってはいけないタイミングは

  • 複数のPromiseを順番関係なく全部待ちたい時

AsyncIterable

基本的に for await...of は AsyncIterable のためにあると言って良いと思います。
AsyncIterable は非同期処理を直列に待つためのプロトコルです。

分かりやすい実例を挙げると、OpenAI の ChatCompletion の API には、ストリーミングでレスポンスを受け取る機能があります。
GPTが生成するテキストを、全部生成し終わってから受け取るのではなく、トークン(≒文字、もしくは単語のような区切り)ごとに受け取ることができるので、ユーザーの待機時間を感覚的に短くすることができます。

その API の Node のライブラリの書き方は例えばこんな感じです。

// README からコピペ
const stream = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [{ role: 'user', content: 'Say this is a test' }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
// トークンが(まるで人間がタイピングしているかのように)一つ一つ標準出力に表示される

このケースの場合、非同期な処理を順番に待つことが重要です。
なぜならLLMが生成するのは自然言語なので、トークンを生成された順番に並べないと意味不明な文章が出来上がるからです。

そして、上記の streamAsyncIterable なオブジェクトです。
Iterable でも ReadableStream でもないので、通常の for...of で回したり stream.getReader() したりすることはできません。

記法を一つに絞っているところに意思を感じるので個人的には好きです。
LLMのAPIのレスポンスに関しては本当にただただストリーミングされたトークンを受け取って逐次表示する以外の用法は無いはずなので、シンプルで良いですよね。

ReadableStream

JS にはストリームを受け取るための ReadableStream というクラスが存在します。例えば fetch のレスポンスの bodyReadableStream です。
ReadableStreamAsyncIterable プロトコルを実装しているので、 for await...of で直列に回すことができます。

for await...of を使わずに getReader() を使うと少々プリミティブな書き味になりアプリケーションコードとしては微妙に使い勝手が悪いです。
一方で for await...of を使うと、単なる配列を同期的にループするような感覚でストリーミングのレスポンスを受け取ることができます。

が、現時点では ReadableStreamAsyncIterable が実装されているランタイムは結構限られています。
ブラウザでほぼ(?)使えないのがツラいですね。
(MDN見る限りFireFoxは使えるっぽいんですが、試したところ動かず。多分Experimentalフラグとかをいじらないとダメな段階かな)
Nodeではv16.5.0から入っているようなので、サーバアプリケーションのコードでなら問題ないかと思います。

Promiseの配列を扱う場合

AsyncIterable でなくとも、複数の Promise を直列に await して処理したい場面というのは実際問題あります。
例えば複数のAPIを順番に繋ぐ必要があるケースで考えます。

const apiResponsePromises = [fetch("api1"), fetch("api2"), fetch("api3")];

こういう複数のPromiseを直列に待ちたい場合、最もポピュラーで簡単で可読性が良いのは for...of を使って await する方法だと思います。

for (const responsePromise of apiResponsePromises) {
  const response = await responsePromise;
  response.xxx
  ...
}

この書き方なら直列で待つことができ、独自の関数を用意する必要もなく、可読性も良いです。
(直列に待ちたい場合に forEach などが使えない理由はここでは流石に省きます)

でこの場合に for await...of を使ってもOKです。

for await (const response of apiResponsePromises) {
  response.xxx
  ...
}

MDN が分かりやすいですが、for await...of の処理の一部を抜き出すと以下の感じです。

  1. 対象のオブジェクトが AsyncIterable かどうか判定。
  2. 対象のオブジェクトが AsyncIterable ではない場合、通常の for...of と同様に、対象のオブジェクトを Iterable なオブジェクトとしてループする。
  3. その Iterable の値が Promise なら、await した上で変数に格納する。

この使い方に関しては通常の for...of + await のシンタックスシュガーと言えるでしょう。

なので、この場合は for await...of を使う方が記述量は減ってスッキリはします。

がしかし、前述の通り認知度の低い記法なので、驚き最小の原則的な意味で個人的には for...of で書いてても全く問題ないと感じます。
そもそも for 文を使っている時点で今のJS/TSの流れだとちょっと抵抗あったりしますし、スッキリすると言っても1行だけで、変数のスコープを狭くできるわけでも無いので...

あるいはチームみんなで共有して使ってこ〜みたいなのが良いかもしれないですね。

順番関係なく全部待つ場合

これはもう色々なところで説明されているので細かい説明は不要かと思います。

Promise.allPromise.allSettled を使いましょう。

おしまい

ということで、直列Promiseにちょっと強くなりましたね。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1