Kodokon kodokon.com

イテレータ、ジェネレータ、そして反復プロトコル

for...of、スプレッド、分割代入の背後にあるプロトコルを習得し、ジェネレータを使って遅延評価のシーケンスを生成します。

9 分 · 3 問

このレッスンを Kodokon で開く

反復を支配するのは 2 つの異なるプロトコルです。オブジェクトが Symbol.iterator キーの下にメソッドを公開しているとき、そのオブジェクトはイテラブル(iterable)です。そのメソッドはイテレータ(iterator)、つまり { value, done } を返す next() メソッドを持つオブジェクトを返さなければなりません。シーケンスを消費するものすべて(for...of、スプレッド演算子 ...、分割代入、Array.fromPromise.all)はこれらのプロトコルを経由します。したがって、どんなオブジェクトも互換にできるのです。

JAVASCRIPT
const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3]
クラスもジェネレータも使わず、手書きしたイテラブル。

イテレータを手書きするのは冗長です。ジェネレータfunction*)はイテレータを自動的に生成します。各 yield は実行を中断し、関数のローカルな状態を保持します。この遅延評価(lazy evaluation)により、消費側が要求する値の数を制限するかぎり、無限のシーケンスをモデル化できます。

JAVASCRIPT
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

function* take(iterable, count) {
  let index = 0;
  for (const value of iterable) {
    if (index >= count) return;
    yield value;
    index += 1;
  }
}

console.log([...take(naturals(), 4)]); // [0, 1, 2, 3]
決してブロックせずに消費される無限のシーケンス。

ジェネレータのイテレータは return()throw() も公開しています。見過ごされがちな仕様上の詳細があります。for...of ループを breakreturn、または例外で抜けると、エンジンは自動的に iterator.return() を呼び出します。ジェネレータでは、これによって保留中の finally ブロックが実行されます。ここはリソース(ファイル、コネクション、ロック)を解放するのに理想的な場所です。

JAVASCRIPT
function* readLines() {
  try {
    yield "line 1";
    yield "line 2";
  } finally {
    console.log("cleanup");
  }
}

for (const line of readLines()) {
  console.log(line);
  break; // triggers return(), hence the finally
}
// line 1, cleanup
break は return() を呼び出します。すると finally ブロックが実行されます。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. プロトコルの意味で、オブジェクトをイテラブルにするものは何ですか?
    • length プロパティと数値インデックスを持つこと
    • next() を持つオブジェクトを返す Symbol.iterator メソッドを公開すること
    • Array.prototype を継承すること
    • next() メソッドを直接持つこと
  2. ジェネレータを消費している最中に for...of ループを break で抜けると、何が起こりますか?
    • 特別なことは起こらず、ジェネレータは最後の yield で中断したままになる
    • エンジンがイテレータの throw() を呼び出す
    • エンジンが return() を呼び出し、それがジェネレータの finally ブロックを実行する
    • 次の反復でジェネレータが最初から再開する
  3. なぜプログラムをブロックせずに無限ジェネレータを扱えるのですか?
    • エンジンが反復回数を自動的に上限で抑える
    • ジェネレータは別のスレッドで実行される
    • 評価が遅延している。各値は next() が呼ばれたときにのみ計算される
    • 値はエンジンによって事前計算されキャッシュされる