TypeScriptとジェネレータ

はじめに

ちょっとジェネレータを使いたいケースが出てきたので仕様を確認しようと思ったら、昔書いたテキストを発掘したのでメモも兼ねて公開。 C#をよく使ってた頃に書いたものなので、それと照らし合わせながらの内容になっています。

ジェネレータとは

C#でいうところのイテレータ構文。ジェネレータ関数と呼ばれるものから返されるオブジェクトで、このオブジェクトはiterableなのでfor...ofを使って列挙することが可能。

ジェネレータ関数

ジェネレータ関数とはfunction*宣言(アスタリスクがついていることが大事)によって定義された関数で、ジェネレータオブジェクトを生成してそれを返す関数。返されたジェネレータオブジェクトのnextメソッドをコールすると、ジェネレータ関数内のyieldが出現するところまで処理が進みポーズ状態となる。yield式に値が設定されている場合は、nextメソッドの戻り値に指定された値が含まれたオブジェクトが返される。

function* generator() {
    console.log("one")
    yield 1
    console.log("two")
    yield 2
    console.log("three")
    yield 3
}

const g = generator()

console.log(g.next())
// [標準出力]
// one
// { value: 1, done: false }

console.log(g.next())
// [標準出力]
// two
// { value: 2, done: false }

console.log(g.next())
// [標準出力]
// three
// { value: 3, done: false }

console.log(g.next())
// [標準出力]
// { value: undefined, done: true}

C#のイテレータ構文と同様、nextがコールされたあとジェネレータ関数の処理がどこまで進んでいるかの状態は内部に保存される。もう一度nextをコールすると、前回のyield式の続きから処理が継続される。C#の場合はイテレータ構文が最後まで達したあとでもCurrentプロパティから最後のyield式の値を取得できたが、TypeScriptではundefinedとなる。

yield*式

複数のジェネレータ関数を組み合わせることもできる。

function* anothereGenerator(i: number) {
    yield i + 1
    yield i + 2
    yield i + 3
}

function* generator(i: number) {
    yield i
    yield* anothereGenerator(i)
    yield i + 10
}

const g = generator(10)

console.log(g.next().value) // 10
console.log(g.next().value) // 11
console.log(g.next().value) // 12
console.log(g.next().value) // 13
console.log(g.next().value) // 20
console.log(g.next().value) // undefined

ジェネレータ関数の中でyield*式にて別のジェネレータ関数を指定することによって、自身の中に別のジェネレータオブジェクトを展開できる。

ジェネレータ関数内でのreturn

C#でいうところのyield break。ジェネレータ関数内でreturnすると、そこでiterateは停止する。

function* generator() {
    console.log("one")
    yield 1
    console.log("two")
    yield 2
    return
    console.log("three")
    yield 3
}

const g = generator()

console.log(g.next())
// [標準出力]
// one
// { value: 1, done: false }

console.log(g.next())
// [標準出力]
// two
// { value: 2, done: false }

console.log(g.next())
// [標準出力]
// { value: undefined, done: true}

console.log(g.next())
// [標準出力]
// { value: undefined, done: true}

returnに値を指定すれば、iterateが停止したときにその値が返ってくる。

function* generator() {
    console.log("one")
    yield 1
    console.log("two")
    yield 2
    return 'end'
    console.log("three")
    yield 3
}

const g = generator()

console.log(g.next())
// [標準出力]
// one
// { value: 1, done: false }

console.log(g.next())
// [標準出力]
// two
// { value: 2, done: false }

console.log(g.next())
// [標準出力]
// { value: 'end', done: true}

console.log(g.next())
// [標準出力]
// { value: undefined, done: true}

ジェネレータに値を渡す

ジェネレータはyieldを使って呼び出し元に値を渡すことができるが、反対にnextメソッドに値を渡すことによって呼び出し元からジェネレータに値を渡すこともできる。

function* sampleGenerator() {
    const num1 = yield 1
    console.log(`inside generator: ${num1}`)
    const num2 = yield 2
    console.log(`inside generator: ${num2}`)
    const num3 = yield 3
    console.log(`inside generator: ${num3}`)
}

const g = sampleGenerator()

console.log(`call next: ${g.next(10).value}`)  // ①
console.log(`call next: ${g.next(20).value}`)  // ②
console.log(`call next: ${g.next(30).value}`)  // ③
console.log(`call next: ${g.next(40).value}`)  // ④

実行結果は以下の通りとなる。

call next: 1
inside generator: 20
call next: 2
inside generator: 30
call next: 3
inside generator: 40
call next: undefined

実装と結果を照らし合わせていけばわかるが、まず①にて最初のyiledに到達するが、ここではnum1に値はセットされない。num1には②でのnextに渡した値がセットされる。そのような形でyield式の戻り値にはyieldに到達したあとの次のnext呼び出し時に渡された値が渡ってくる。

反復処理

TypeScriptには繰り返し構文としてfor..ofが用意されている(C#でいうところのfor-each)。オブジェクトがfor..ofの対象として利用できる条件としてiterableプロトコルに準拠する、といったものがある。これはC#でいうところのIEnumerableインタフェースと同じようなものだが、JavaScriptにはインタフェースがないため、プロトコルといった規約が決められている。

iterableプロトコルの内容は、[Symbol.iterator]というプロパティをもち、このプロパティの値がiteratorプロトコルに順子するオブジェクトを返す関数である、というものである(これもC#と同じようなかんじ)

お察しのとおりiteratorプロトコルもIEnumeratorインタフェースと同じようなもので、bool型のdoneプロパティと任意の型のvalueプロパティを持つオブジェクトを返すnextメソッドを実装しているという条件になる。

ジェネレータオブジェクトはこのiteratrableプロトコルとiteratorプロトコルに準拠するので、for..ofを使って反復処理ができる。

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return(value: TReturn): IteratorResult<T, TReturn>;
    throw(e: any): IteratorResult<T, TReturn>;
    [Symbol.iterator](): Generator<T, TReturn, TNext>;
}
function* generator():Generator<number, void, unknown> {
    yield 1
    yield 2
    yield 3
}

const gen = generator()

for(const v of gen) {
    console.log(v)
}

// [標準出力]
// 1
// 2
// 3

Generatorの型パラメータ

見ての通り、ジェネレータオブジェクトには3つの型パラメータが定義されている。

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext>

それぞれ何を表しているかというと

Column 1 Column 2
T yield式の値の型
TReturn yield return式の値の型
TNext nextメソッドの引数の型

ジェネレータ関数から配列を作る

Array.fromメソッドは引数でジェネレータオブジェクトを受け取って配列を作ることができる。

function* generator() {
    yield 1
    yield 2
    yield 3
    yield 4
    yield 5
}

const array = Array.from(generator())
console.log(array) // [ 1, 2, 3, 4, 5 ]
Profile
d_yama
元Microsoft MVP for Windows Development(2018-2020)
Sub-category : Windows Mixed Reality
Search