TypeScriptのイテレーターとジェネレーター
目次
ECMAScriptの反復処理インターフェース
ECMAScript(現行: 2023)では、反復処理(iteration)のためのインターフェースとして、
の5つが定義されている。
Iterable
Iterableは、Iteratorにしたがうオブジェクトを返す関数@@iterator(Symbol.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にしたがうオブジェクトを返す関数@@asyncIterator(Symbol.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を返す関数return、throwを持つことを示すインターフェースとなっている。
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の仕様には、一度doneがtrueのIteratorResultが返されたら、それ以降返されるIteratorResultのdoneはすべてtrueであるべき旨が書かれているが、一方でこの要求は強制的なものではないとも書かれている。
またECMAScriptの仕様には、Iteratorはnextが引数なしで呼ばれることもあることを前提としなければならない旨の注釈も付け加えられてある。TypeScriptでのnextの引数がタプルで表現されているのはそのチェックを厳密にするためであると思われる(省略したソースコード中のコメントにそのような旨が記載されている)。
returnとthrowはそれぞれIteratorに反復処理の終了やエラーによる中断を知らせるためのもので、ECMAScriptの仕様では、典型的な振る舞いはvalueで指定した値をvalueとして持つIteratorResultを返したり、eで指定した値をthrowしたりして、以降のIteratorResultのdoneをtrueにすることであるが、実際の振る舞いはIteratorがどのように実装されているかに依存するとされている。
AsyncIterator
AsyncIteratorは、Iteratorの非同期版で、プロパティとしてもつ関数がすべて、IteratorResultの代わりにIteratorResultのPromiseを返すインターフェスである。
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は、doneとvalueの二つの値をプロパティーとして持ちうるインターフェースである。doneはtrueまたはfalse、valueは任意のECMAScriptの値をとるが、存在しない可能性もある。
doneがfalseまたは存在しない場合、valueの値は反復の一要素として解釈される。一方、doneがtrueである場合、それは反復の終了を意味し、そのときの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)を返す@@iterator(Symbol.iterator)をプロパティとして持つプロトタイプ%IteratorPrototype%を継承しているため、イテレーター自身もIterableインターフェースを満たしている。
これをTypeScriptでは次のように再帰的に表現している。
src/lib/es2015.iterable.d.ts#L34-L36
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}
これを使って例えばArrayのinterfaceの一部は、次のように定義されている。
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)を返す@@asyncIterator(Symbol.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との違いは、returnとthrow、およびそれらの引数にあった?がついていない点で、典型的なIteratorの振る舞いとして先に述べておいた、returnの場合valueをそのまま返して終了し、throwの場合eをthrowして終了する、という既定の動作が自動で定義されるようになっている。
また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)
参考リンク
- MDN: JavaScript
- Guide: Iterators and Generators
- Reference: Iteration protocols
- Reference:
function* - Reference:
function*expression - Reference:
yield - Reference:
yield* - Reference:
for...of - Reference:
for await...of - Reference:
Symbol.iterator - Reference:
Symbol.asyncIterator - Reference:
Generator - Reference:
GeneratorFunction
- TypeScript: Documentation
- Iterators and Generators
- TypeScript: Source
src/lib/es2015.symbol.d.tssrc/lib/es2015.iterable.d.tssrc/lib/es2015.generator.d.tssrc/lib/es2018.asynciterable.d.tssrc/lib/es2018.asyncgenerator.d.ts
- ECMAScript® Language Specification
- 6.1.5.1 Well-Known Symbols
- 7.4 Operations on Iterator Objects
- 13.15 Assignment Operators
- 14.7 Iteration Statements
- 15.5 Generator Function Definitions
- 15.6 Async Generator Function Definitions
- 27.1 Iteration
- 27.3 GeneratorFunction Objects
- 27.4 AsyncGeneratorFunction Objects
- 27.5 Generator Objects
- 27.6 AsyncGenerator Objects