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.ts
src/lib/es2015.iterable.d.ts
src/lib/es2015.generator.d.ts
src/lib/es2018.asynciterable.d.ts
src/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