JavaScript for impatient programmers (ES2022版)
この本の支援をお願いします:購入する または 寄付する
(広告、ブロックしないでください。)

42 非同期イテレーション



  必要な知識

この章では、以下の知識が必要です。

42.1 基本的な非同期イテレーション

42.1.1 プロトコル:非同期イテレーション

非同期イテレーションの動作を理解するために、まず同期イテレーションを再確認しましょう。これは以下のインターフェースで構成されます。

interface Iterable<T> {
  [Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
  next() : IteratorResult<T>;
}
interface IteratorResult<T> {
  value: T;
  done: boolean;
}

非同期イテレーションのプロトコルでは、変更したいのは1点だけです。.next()によって生成される値を非同期的に配信する必要があります。考えられるオプションは2つあります。

言い換えれば、Promiseでラップする対象が値だけか、イテレーター結果全体かという問題です。

後者でなければなりません。なぜなら、.next()が結果を返すとき、非同期計算が開始されるからです。その計算が値を生成するか、イテレーションの終了を通知するかは、計算が完了するまで判断できません。したがって、.done.valueの両方をPromiseでラップする必要があります。

非同期イテレーションのインターフェースは以下のようになります。

interface AsyncIterable<T> {
  [Symbol.asyncIterator]() : AsyncIterator<T>;
}
interface AsyncIterator<T> {
  next() : Promise<IteratorResult<T>>; // (A)
}
interface IteratorResult<T> {
  value: T;
  done: boolean;
}

同期インターフェースとの唯一の違いは、.next()の戻り値の型です(A行)。

42.1.2 非同期イテレーションの直接的な使用

次のコードは、非同期イテレーションプロトコルを直接使用しています。

const asyncIterable = syncToAsyncIterable(['a', 'b']); // (A)
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

// Call .next() until .done is true:
asyncIterator.next() // (B)
.then(iteratorResult => {
  assert.deepEqual(
    iteratorResult,
    { value: 'a', done: false });
  return asyncIterator.next(); // (C)
})
.then(iteratorResult => {
  assert.deepEqual(
    iteratorResult,
    { value: 'b', done: false });
  return asyncIterator.next(); // (D)
})
.then(iteratorResult => {
  assert.deepEqual(
    iteratorResult,
     { value: undefined, done: true });
})
;

A行では、値 'a' と 'b' について非同期イテラブルを作成します。syncToAsyncIterable()の実装は後で説明します。

B行、C行、D行で.next()を呼び出します。毎回、.then()を使用してPromiseを展開し、assert.deepEqual()を使用して展開された値を確認します。

非同期関数を使用すれば、このコードを簡素化できます。これで、awaitを使ってPromiseを展開し、同期イテレーションをしているかのように見えます。

async function f() {
  const asyncIterable = syncToAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  
  // Call .next() until .done is true:
  assert.deepEqual(
    await asyncIterator.next(),
    { value: 'a', done: false });
  assert.deepEqual(
    await asyncIterator.next(),
    { value: 'b', done: false });
  assert.deepEqual(
    await asyncIterator.next(),
    { value: undefined, done: true });
}

42.1.3 for-await-ofによる非同期イテレーションの使用

非同期イテレーションプロトコルは、直接使用することを意図していません。それをサポートする言語構成の1つがfor-await-ofループです。これはfor-ofループの非同期バージョンです。非同期関数と非同期ジェネレーター(この章の後半で説明します)で使用できます。これがfor-await-ofの使用例です。

for await (const x of syncToAsyncIterable(['a', 'b'])) {
  console.log(x);
}
// Output:
// 'a'
// 'b'

for-await-ofは比較的柔軟です。非同期イテラブルに加えて、同期イテラブルもサポートします。

for await (const x of ['a', 'b']) {
  console.log(x);
}
// Output:
// 'a'
// 'b'

そして、Promiseでラップされた値に対する同期イテラブルもサポートします。

const arr = [Promise.resolve('a'), Promise.resolve('b')];
for await (const x of arr) {
  console.log(x);
}
// Output:
// 'a'
// 'b'

  練習問題:非同期イテラブルを配列に変換する

警告:この練習問題の解答はこの章で後ほど示します。

42.2 非同期ジェネレーター

非同期ジェネレーターは、同時に2つのものです。

  非同期ジェネレーターは同期ジェネレーターと非常によく似ています

非同期ジェネレーターと同期ジェネレーターは非常に似ているため、yieldyield*の正確な動作については説明しません。疑問がある場合は、§38「同期ジェネレーター」を参照してください。

したがって、非同期ジェネレーターには以下があります。

これは以下のようになります。

async function* asyncGen() {
  // Input: Promises, async iterables
  const x = await somePromise;
  for await (const y of someAsyncIterable) {
    // ···
  }

  // Output
  yield someValue;
  yield* otherAsyncGen();
}

42.2.1 例:非同期ジェネレーターによる非同期イテラブルの作成

例を見てみましょう。次のコードは、3つの数値を持つ非同期イテラブルを作成します。

async function* yield123() {
  for (let i=1; i<=3; i++) {
    yield i;
  }
}

yield123()の結果は、非同期イテレーションプロトコルに準拠していますか?

async function check() {
  const asyncIterable = yield123();
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  assert.deepEqual(
    await asyncIterator.next(),
    { value: 1, done: false });
  assert.deepEqual(
    await asyncIterator.next(),
    { value: 2, done: false });
  assert.deepEqual(
    await asyncIterator.next(),
    { value: 3, done: false });
  assert.deepEqual(
    await asyncIterator.next(),
    { value: undefined, done: true });
}
check();

42.2.2 例:同期イテラブルの非同期イテラブルへの変換

次の非同期ジェネレーターは、同期イテラブルを非同期イテラブルに変換します。これは、前に使用したsyncToAsyncIterable()関数を実装しています。

async function* syncToAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem;
  }
}

注:この場合、入力は同期です(awaitは必要ありません)。

42.2.3 例:非同期イテラブルの配列への変換

次の関数は、前の練習問題の解答です。非同期イテラブルを配列に変換します(スプレッド構文ですが、同期イテラブルではなく非同期イテラブル用です)。

async function asyncIterableToArray(asyncIterable) {
  const result = [];
  for await (const value of asyncIterable) {
    result.push(value);
  }
  return result;
}

この場合、非同期ジェネレーターは使用できません。入力はfor-await-ofで取得し、Promiseでラップされた配列を返します。後者の要件により、非同期ジェネレーターは除外されます。

これはasyncIterableToArray()のテストです。

async function* createAsyncIterable() {
  yield 'a';
  yield 'b';
}
const asyncIterable = createAsyncIterable();
assert.deepEqual(
  await asyncIterableToArray(asyncIterable), // (A)
  ['a', 'b']
);

A行のawaitに注目してください。これは、asyncIterableToArray()によって返されるPromiseを展開するために必要です。awaitが機能するためには、このコード断片を非同期関数内で実行する必要があります。

42.2.4 例:非同期イテラブルの変換

既存の非同期イテラブルを変換して新しい非同期イテラブルを生成する非同期ジェネレーターを実装しましょう。

async function* timesTwo(asyncNumbers) {
  for await (const x of asyncNumbers) {
    yield x * 2;
  }
}

この関数をテストするために、前のセクションのasyncIterableToArray()を使用します。

async function* createAsyncIterable() {
  for (let i=1; i<=3; i++) {
    yield i;
  }
}
assert.deepEqual(
  await asyncIterableToArray(timesTwo(createAsyncIterable())),
  [2, 4, 6]
);

  練習問題:非同期ジェネレーター

警告:この練習問題の解答はこの章で後ほど示します。

42.2.5 例:非同期イテラブルに対するマッピング

念のため、同期イテラブルに対するマッピングの方法を示します。

function* mapSync(iterable, func) {
  let index = 0;
  for (const x of iterable) {
    yield func(x, index);
    index++;
  }
}
const syncIterable = mapSync(['a', 'b', 'c'], s => s.repeat(3));
assert.deepEqual(
  Array.from(syncIterable),
  ['aaa', 'bbb', 'ccc']);

非同期バージョンは以下のようになります。

async function* mapAsync(asyncIterable, func) { // (A)
  let index = 0;
  for await (const x of asyncIterable) { // (B)
    yield func(x, index);
    index++;
  }
}

同期実装と非同期実装がどのように似ているかに注目してください。唯一の違いは、A行のasyncとB行のawaitです。これは、同期関数から非同期関数への変換と似ています。asyncキーワードと場合によってはawaitを追加するだけです。

mapAsync()をテストするために、ヘルパー関数asyncIterableToArray() (この章で前に示した) を使用します。

async function* createAsyncIterable() {
  yield 'a';
  yield 'b';
}
const mapped = mapAsync(
  createAsyncIterable(), s => s.repeat(3));
assert.deepEqual(
  await asyncIterableToArray(mapped), // (A)
  ['aaa', 'bbb']);

再び、Promiseを展開するためにawaitを使用します(A行)。このコード断片は、非同期関数内で実行する必要があります。

  練習問題:filterAsyncIter()

exercises/async-iteration/filter_async_iter_test.mjs

42.3 Node.jsストリームに対する非同期イテレーション

42.3.1 Node.jsストリーム:コールバックによる非同期処理(プッシュ)

従来、Node.jsストリームからの非同期読み取りは、コールバックを使用して行われていました。

function main(inputFilePath) {
  const readStream = fs.createReadStream(inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 });
  readStream.on('data', (chunk) => {
    console.log('>>> '+chunk);
  });
  readStream.on('end', () => {
    console.log('### DONE ###');
  });
}

つまり、ストリームが制御を行い、データを読取側にプッシュします。

42.3.2 Node.jsストリーム:非同期イテレーションによる非同期処理(プル)

Node.js 10以降、非同期イテレーションを使用してストリームから読み取ることもできます。

async function main(inputFilePath) {
  const readStream = fs.createReadStream(inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 });

  for await (const chunk of readStream) {
    console.log('>>> '+chunk);
  }
  console.log('### DONE ###');
}

今回は、読取側が制御を行い、ストリームからデータプルします。

42.3.3 例:チャンクから行への変換

Node.jsストリームは、データのチャンク(任意の長さの断片)を反復処理します。次の非同期ジェネレーターは、チャンクに対する非同期イテラブルを行に対する非同期イテラブルに変換します。

/**
 * Parameter: async iterable of chunks (strings)
 * Result: async iterable of lines (incl. newlines)
 */
async function* chunksToLines(chunksAsync) {
  let previous = '';
  for await (const chunk of chunksAsync) { // input
    previous += chunk;
    let eolIndex;
    while ((eolIndex = previous.indexOf('\n')) >= 0) {
      // line includes the EOL (Windows '\r\n' or Unix '\n')
      const line = previous.slice(0, eolIndex+1);
      yield line; // output
      previous = previous.slice(eolIndex+1);
    }
  }
  if (previous.length > 0) {
    yield previous;
  }
}

chunksToLines()を、チャンクに対する非同期イテラブル(chunkIterable()によって生成されるもの)に適用してみましょう。

async function* chunkIterable() {
  yield 'First\nSec';
  yield 'ond\nThird\nF';
  yield 'ourth';
}
const linesIterable = chunksToLines(chunkIterable());
assert.deepEqual(
  await asyncIterableToArray(linesIterable),
  [
    'First\n',
    'Second\n',
    'Third\n',
    'Fourth',
  ]);

これで、行に対する非同期イテラブルができましたので、前の練習問題の解答であるnumberLines()を使用して、それらの行に番号を付けることができます。

async function* numberLines(linesAsync) {
  let lineNumber = 1;
  for await (const line of linesAsync) {
    yield lineNumber + ': ' + line;
    lineNumber++;
  }
}
const numberedLines = numberLines(chunksToLines(chunkIterable()));
assert.deepEqual(
  await asyncIterableToArray(numberedLines),
  [
    '1: First\n',
    '2: Second\n',
    '3: Third\n',
    '4: Fourth',
  ]);