たれぱんのびぼーろく

わたしの備忘録、生物学とプログラミングが多いかも

Async Generatorの使いどころ

A: 逐次処理と中間結果利用を、関心に基づいて分離できる

Async Generator (Async Iterator with Generator) とは

async function*で定義できるGeneratorのこと。
例:

// Delay utility
const asyncProcessing = (output: number): Promise<number> =>
  new Promise((resolve) => setTimeout(() => resolve(output), 5 * 1000));

// Generator definition
async function* AsyncGenerator() {
  yield { type: "start", payload: "start processing" };

  const step1 = await asyncProcessing(1);
  yield { type: "processing", payload: "step1 finished" };

  await asyncProcessing(step1);
  yield { type: "processing", payload: "step2 finished" };

  yield { type: "end", payload: "processed" };
}

// Usecase
(async () => {
  for await (const action of AsyncGenerator()) {
    console.log(JSON.stringify(action));
  }
})();
// { type: "start", payload: "start processing" }
// (wait 5 sec)
// { type: "processing", payload: "step1 finished" }
// (wait 5 sec)
// { type: "processing", payload: "step2 finished" }
// { type: "end", payload: "processed" }

何がうれしいの?

A: 中間状態を綺麗に「外に出す」ことができる.

非同期処理を逐次的におこなうパターンを考える.
終結果はもちろん大事だけど、中間状態にもしばしば利用価値がある.
例えばUIでプログレスバー(進捗状況、30%完了的な)を出すとか.
例えば通知を飛ばすとか("type:Afternoonまで終了しました"通知とか).

そして「一連の処理」と「中間状態の利用」が別の関心事の場合がよくある.
関心の分離/単一責任原則に則れば、一連の処理は一連の処理で閉じているべき.
しかしasync/awaitでは途中結果をスマートに外に出す方法がない.
notify()的な関数を引数にとって外部へメッセージングするしかない.

async function notBeautifulAsync(notify: Function) {
  notify({ type: "start", payload: "start processing" });

  const step1 = await asyncProcessing(1);
  notify({ type: "processing", payload: "step1 finished" });

  await asyncProcessing(step1);
  notify({ type: "processing", payload: "step2 finished" });

  return { type: "end", payload: "processed" };
}

処理を書きたいだけなのに、よくわからんnotify関数を叩かなきゃいけない(それも返り値のない副作用の塊を)

そこで出てくるのがGenerator.
一定の区切りごとにyieldするだけで外部へメッセージングができる.
外部はyieldされてきた値の"段階"を知る術がない(step1が終わったメッセージなのかstep2なのかわからない)が、stepを考えないか、あるいは{type: "stepX", payload: "anything"}yieldするかで対応すればいい.
これで処理は処理のみを、中間状態の利用は利用のみを、関心を分離して扱える.