Public Theta Blog

TypeScriptのイテレーターとジェネレーター

ECMAScriptの反復処理インターフェース

ECMAScript(現行: 2023)では、反復処理(iteration)のためのインターフェースとして、

の5つが定義されている。

Iterable

Iterableは、Iteratorにしたがうオブジェクトを返す関数@@iteratorSymbol.iterator)をプロパティとして必ず持つことを示すインターフェースである。

TypeScriptではそのまま次のように定義されている。

src/lib/es2015.iterable.d.ts#L30-L32

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

※引用するコードにもともとあったコメントは省略してある。以下同様。

この@@iteratorは、for...of文、yield*式、...演算子などを使った際にも暗黙的に呼ばれる(抽象操作GetIteratorを参照)。

for (const item of [1, 2, 3, 4]) {
    console.log(item)
}

AsyncIterable

AsyncIterableは、Iterableの非同期版で、AsyncIteratorにしたがうオブジェクトを返す関数@@asyncIteratorSymbol.asyncIterator)をプロパティとして必ず持つことを示すインターフェースである。

こちらもTypeScriptではそのまま次のように定義されている。

src/lib/es2018.asynciterable.d.ts#L19-L21

interface AsyncIterable<T> {
    [Symbol.asyncIterator](): AsyncIterator<T>;
}

この@@asyncIteratorも、for await...of文、yield*式、などを使った際に暗黙的に呼ばれうる(同じく抽象操作GetIteratorを参照)。

Iterator

Iteratorは、IteratorResultを返す関数nextをプロパティとして必ず持ち、オプションとして同じくIteratorResultを返す関数returnthrowを持つことを示すインターフェースとなっている。

TypeScriptでの定義は次のようになっている。

src/lib/es2015.iterable.d.ts#L23-L28

interface Iterator<T, TReturn = any, TNext = undefined> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

反復はnextを呼ぶことによって進められる。nextは引数を任意で1つとり、IteratorResultを返す。ECMAScriptの仕様には、一度donetrueIteratorResultが返されたら、それ以降返されるIteratorResultdoneはすべてtrueであるべき旨が書かれているが、一方でこの要求は強制的なものではないとも書かれている。

またECMAScriptの仕様には、Iteratornextが引数なしで呼ばれることもあることを前提としなければならない旨の注釈も付け加えられてある。TypeScriptでのnextの引数がタプルで表現されているのはそのチェックを厳密にするためであると思われる(省略したソースコード中のコメントにそのような旨が記載されている)。

returnthrowはそれぞれIteratorに反復処理の終了やエラーによる中断を知らせるためのもので、ECMAScriptの仕様では、典型的な振る舞いはvalueで指定した値をvalueとして持つIteratorResultを返したり、eで指定した値をthrowしたりして、以降のIteratorResultdonetrueにすることであるが、実際の振る舞いはIteratorがどのように実装されているかに依存するとされている。

AsyncIterator

AsyncIteratorは、Iteratorの非同期版で、プロパティとしてもつ関数がすべて、IteratorResultの代わりにIteratorResultPromiseを返すインターフェスである。

TypeScriptでの定義は次のようになっている。

src/lib/es2018.asynciterable.d.ts#L12-L17

interface AsyncIterator<T, TReturn = any, TNext = undefined> {
    next(...args: [] | [TNext]): Promise<IteratorResult<T, TReturn>>;
    return?(value?: TReturn | PromiseLike<TReturn>): Promise<IteratorResult<T, TReturn>>;
    throw?(e?: any): Promise<IteratorResult<T, TReturn>>;
}

IteratorResult

イテレーターから返されるIteratorResultは、donevalueの二つの値をプロパティーとして持ちうるインターフェースである。donetrueまたはfalsevalueは任意のECMAScriptの値をとるが、存在しない可能性もある。

donefalseまたは存在しない場合、valueの値は反復の一要素として解釈される。一方、donetrueである場合、それは反復の終了を意味し、そのときのvalueはイテレーター自体の返却値として解釈される。

これをTypeScriptでは次のように表現している。

src/lib/es2015.iterable.d.ts#L11-L21

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

組み込みの反復可能オブジェクト

ECMAScriptで定義されている次の組み込みオブジェクトはIterableインターフェースを満たしてる。

そのため、これらのオブジェクトは、for...ofなどで直接反復させることができる。

for (const char of "abc") {
  console.log(char)
}

イテレーターの反復可能性

IterableIterator

ECMAScriptで定義されている上記のようなIteratorを満たすオブジェクトは、そのイテレーター自身(this)を返す@@iteratorSymbol.iterator)をプロパティとして持つプロトタイプ%IteratorPrototype%を継承しているため、イテレーター自身もIterableインターフェースを満たしている。

これをTypeScriptでは次のように再帰的に表現している。

src/lib/es2015.iterable.d.ts#L34-L36

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

これを使って例えばArrayinterfaceの一部は、次のように定義されている。

src/lib/es2015.iterable.d.ts#L38-L56

interface Array<T> {
    [Symbol.iterator](): IterableIterator<T>;
    entries(): IterableIterator<[number, T]>;
    keys(): IterableIterator<number>;
    values(): IterableIterator<T>;
}

AsyncIterableIterator

ECMAScriptで定義されているAsyncIteratorを満たすオブジェクトもまた、そのイテレーター自身(this)を返す@@asyncIteratorSymbol.asyncIterator)をプロパティとして持つプロトタイプ%AsyncIteratorPrototype%を継承しているため、イテレーター自身がAsyncIterableインターフェースを満たすようになっている。

これもTypeScriptでは同様に次のように再帰的に表現している。

src/lib/es2018.asynciterable.d.ts#L23-L25

interface AsyncIterableIterator<T> extends AsyncIterator<T> {
    [Symbol.asyncIterator](): AsyncIterableIterator<T>;
}

イテレーターと反復可能オブジェクトの作成

先に挙げたインターフェースを満たすようにオブジェクトを作成すれば、自身でイテレーターや反復可能オブジェクトを作成することもできる。

たとえば、次のコードは1から10を出力する。

const ONE_TO_TEN = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next: () => {
        i += 1;
        return i <= 10 ? {
          value: i,
        } : {
          done: true
        }
      }
    }
  }
}

for (const i of ONE_TO_TEN) {
  console.log(i)
}

ジェネレーター関数とジェネレーター

組み込みの反復可能オブジェクトのように、IteratorでありかつIterable、あるいはAsyncIteratorでありかつAsyncIterableであるようなイテレーターを作成するために、ECMAScriptではジェネレーター関数というものが用意されている。

GeneratorFunctionは、IteratorでありかつIterableなオブジェクトGeneratorを返す関数であり、function*を使って定義される。

AsyncGeneratorFunctionはその非同期版で、AsyncIteratorでありかつAsyncIterableなオブジェクトAsyncGeneratorを返す関数であり、async function*を使って定義される。

これらのジェネレーター関数の中では、値を反復の一要素として出力するyieldや、処理を別のジェネレーターや反復可能オブジェクトに委任するyield*が使えるため、反復処理を柔軟に記述することができる。

たとえば、ONE_TO_TENを定義して作成した1から10を出力するコードは、loからhiまで(両端含む)のnumberを列挙するrange関数をジェネレーター関数として定義することで、次のように書き直すことができる。

function* range(lo: number, hi: number): Generator<number> {
  for (let i = lo; i <= hi; i++) {
    yield i;
  }
}

for (const i of range(1, 10)) {
  console.log(i)
}

Generatorは、TypeScriptでは次のように定義されている。

src/lib/es2015.generator.d.ts#L3-L9

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return(value: TReturn): IteratorResult<T, TReturn>;
    throw(e: any): IteratorResult<T, TReturn>;
    [Symbol.iterator](): Generator<T, TReturn, TNext>;
}

IterableIteratorとの違いは、returnthrow、およびそれらの引数にあった?がついていない点で、典型的なIteratorの振る舞いとして先に述べておいた、returnの場合valueをそのまま返して終了し、throwの場合ethrowして終了する、という既定の動作が自動で定義されるようになっている。

またnextに対して与えた引数は、yieldの返却値として与えられるようになっており、ジェネレーター関数内で受け取れるようになっている。

function* makeLogger(limit: number): Generator<void, void, string> {
    for (let i = 0; i < limit; i++) {
        console.log(yield)
    }
}

const logger = makeLogger(3)
logger.next("hello") // "hello"
logger.next("hello") // "hello"
logger.next("hello") // "hello"
logger.next("hello") // 出力されない

async function*を使って定義されるAsyncGeneratorFunctionはその非同期版で、返されるAsyncGeneratorはTypeScriptで次のように定義されている。

src/lib/es2018.asyncgenerator.d.ts#L3-L9

interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> extends AsyncIterator<T, TReturn, TNext> {
    next(...args: [] | [TNext]): Promise<IteratorResult<T, TReturn>>;
    return(value: TReturn | PromiseLike<TReturn>): Promise<IteratorResult<T, TReturn>>;
    throw(e: any): Promise<IteratorResult<T, TReturn>>;
    [Symbol.asyncIterator](): AsyncGenerator<T, TReturn, TNext>;
}

これも返される値(および返される値としてあたえる値)がPromiseとなっていることを除いて、Generatorと同様である。

async function* fetchAllText(urls: string[]): AsyncGenerator<string> {
  for (const url of urls) {
    const res = await fetch(url)
    yield await res.text()
  }
}

async function main() {
  for await (const text of fetchAllText([])) {
    console.log(text)
  }
}

main().catch(console.error)

参考リンク