初めに
今回はジェネレータについてまとめていきたいと思います。
今回の参考文章はこちらです。
Generators - javascript.info
yield - MDN
yield* - MDN
Syntax
function*
:generator関数の構文。
yield
:generator関数内から返す値を定義する。
next()
:generator関数から返却された値を取得する。結果は常にオブジェクト{value: , done: }
。
function* generateSequence() {
yield 1
yield 2
return 3
}
let generator = generateSequence()
console.log(generator) // Object [Generator] {}
console.log(generator.next()) // { value: 1, done: false }
console.log(JSON.stringify(generator.next())) // {"value":2,"done":false}
console.log(generator.next()) // { value: 3, done: true }
console.log(generator.next()) // { value: undefined, done: true }
generator関数は普通の関数のように呼び出すことができません。(実質上はiterableオブジェクトに近いと思う。)
next()
メソッドの呼び出しを通して一番近いyield
文までコードを実行した後一時停止し値を取得する。次のnext()
が呼び出されるとまた再開する。
next()
で最後のyield
の値を取得、あるいはreturn
に到達したらdone: true
になります。最終結果が処理された以上の呼び出しは意味ありません。
iteration
next()
ではなくfor..of
でループ処理で値だけを取り出すことができます。
function* generateSequence() {
yield 1
yield 2
yield 3
}
let generator = generateSequence()
for (let value of generator) {
console.log(value)
}
console.log(generator.next())
// 1
// 2
// 3
// { value: undefined, done: true }
for..of
でジェネレータを実行する場合は、throw
、return
に遭ったら完全に停止されます。
break
なら外側のコードで最初のyield
から実行します。
function* breakGenerator() {
yield 1
yield 2
yield 3
}
for (let value of breakGenerator()) {
console.log(value)
break
}
console.log('done')
console.log(breakGenerator().next())
// 1
// done
// { value: 1, done: false }
そしてスプレッド構文で展開したり、
function* generateSequence() {
yield 1
yield 2
yield 3
}
let sequence = [0, ...generateSequence()]
console.log(sequence) // [ 0, 1, 2, 3 ]
for
ループで反復処理を行ったりすることができます。
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i
}
}
let sequence = [...generateSequence(1, 5)]
console.log(sequence) // [ 1, 2, 3, 4, 5 ]
[Symbol.iterator] in object
[Symbol.iterator]
でオブジェクトにデフォルトのiterator
を設置できます。
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() {
for (let value = this.from; value <= this.to; value++) {
yield value
}
}
}
console.log([...range]) // [ 1, 2, 3, 4, 5 ]
(*[Symbol.iterator]() {}
は短縮記法。)
スプレッド構文ではなく、オブジェクトから[Symbol.iterator]
の呼び出しでnext()
を利用することもできます。
console.log(range[Symbol.iterator]) // [GeneratorFunction: [Symbol.iterator]]
let iterator = range[Symbol.iterator]()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
generator composition
yield*
:外部から別のジェネレータ、あるいは反復可能なオブジェクトに実行を委任する。
function* iterableObj() {
yield* [1, 2]
yield* '34'
yield* Array.from(arguments)
}
let iterator = iterableObj(5, 6)
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: '3', done: false }
console.log(iterator.next()) // { value: '4', done: false }
console.log(iterator.next()) // { value: 5, done: false }
console.log(iterator.next()) // { value: 6, done: false }
yield*
で別のジェネレータへ、
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57)
// A..Z
yield* generateSequence(65, 90)
// a..z
yield* generateSequence(97, 122)
}
let str = ''
for (let code of generatePasswordCodes()) {
str += String.fromCharCode(code)
}
console.log(str)
// 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
about yield
“yield” is a two-way street例の説明では、yield
は外部へ値の返しや、外部のnext()
の引数を内部の値として入れることができます。yield
の動きを見ていきたいと思います。
function* twoWayStreet() {
// yield can output or input value
let result = yield '2 + 2?'
console.log(result)
}
let generator = twoWayStreet()
console.log(generator.next().value) // 2 + 2? // output
// generator.next(4) // 4 // input
console.log(generator.next(4)) // { value: undefined, done: true }
yield
は同時に内部⇔外部、値の転送の仲介を担当しているように見えます。
function* twoWayStreet() {
let result1 = yield '2 + 2?'
console.log(result1)
let result2 = yield '3 + 3?'
console.log(result2)
}
let generator = twoWayStreet()
console.log(generator.next())
// { value: '2 + 2?', done: false }
console.log(generator.next(4))
// 4
// { value: '3 + 3?', done: false }
console.log(generator.next(9))
// 9
// { value: undefined, done: true }
最初コンソールを見て、なぜこんな結果がでるのかわかりませんでした。
next()
呼ばれると次のyield
へ移動し値を取るのに、なぜ二番目のnext
が一番目のyield
に値を入れたのか?
全体の動きとしては、
console.log(generator.next())
// { value: '2 + 2?', done: false }
↓ (一時停止)
generator.next(4)
let result1 = 4
console.log(result1)
// 4
generator.next()
// { value: '3 + 3?', done: false }
↓
generator.next(9)
let result2 = 9
console.log(result2)
// 9
generator.next()
// { value: undefined, done: true }
ほかのコードでテストしてみるとyield
は一定のルールで値を転送することに気づき、上のように整理してみました。
どうやらnext()
の呼び出しは順次にyield
から値を取得するが、next()
に引数を入れた場合は外部から内部、そして内部から外部へ、という順序で値を渡す。
実は上のコード少し変更してみると、
console.log(generator.next(4))
// { value: '2 + 2?', done: false }
console.log(generator.next(9))
// 9
// { value: '3 + 3?', done: false }
console.log(generator.next(5))
// 5
// { value: undefined, done: true }
console.log(generator.next(4))
// まだ一番目のyield
の値実行していないので値の転送先がわかりません。
console.log(generator.next())
// { value: '2 + 2?', done: false }
↓ (一時停止)
generator.next(9)
let result1 = 9
console.log(result1)
// 9
generator.next()
// { value: '3 + 3?', done: false }
↓
generator.next(5)
let result2 = 5
console.log(result2)
// 5
generator.next()
// { value: undefined, done: true }
generator.throw
function* generateThrow() {
try {
let result = yield '2 + 2?'
console.log('The execution does not reach here, because the exception is thrown above')
} catch (err) {
console.log(err)
}
}
let generator = generateThrow()
// console.log(generator) // Object [Generator] {}
generator.throw(new Error('The answer is not found in my database'))
// Error: The answer is not found in my database
next()
のほか、throw()
メソッドでエラーを投げることもできます。
next()
のようにthrow
も一番近いyield
文へ値を転送、そしてtry...catch
文でエラーを発見したとたんにすぐcatch(err)
へ移動しました。
下の例はスロー無しで試してみました。
function* generateThrow() {
try {
let result = yield error()
console.log('The execution does not reach here, because the exception is thrown above')
} catch (err) {
console.log(err)
}
}
let generator = generateThrow()
function error() {
throw new Error('Generate Error!')
}
generator.next()
// Error: Generate Error!
generator.return
function* generatorReturn() {
yield 1
yield 2
yield 3
}
let generator = generatorReturn()
console.log(generator.next()) // { value: 1, done: false }
console.log(generator.return('stop')) // { value: 'stop', done: true }
console.log(generator.next()) // { value: undefined, done: true }
ここreturn()
の動きはnext()
とちょっと違うと思いますが、下の順序少し変更してみたら、
console.log(generator.return('stop')) // { value: 'stop', done: true }
console.log(generator.next()) // { value: undefined, done: true }
console.log(generator.next()) // { value: undefined, done: true }
next(4)
は外部⇒内部、内部⇒外部、といった既定のルートによって値を転送するけど、return()
は値をvalue
へ渡し、強制的にジェネレータを閉じる(done: true
)ように見えます。
Pseudo-random generator
function* pseudoRandom(seed) {
let value = seed
while (true) {
value = value * 16807 % 2147483647
yield value
}
}
let generator = pseudoRandom(1)
console.log(generator.next().value) // 16807
console.log(generator.next().value) // 282475249
console.log(generator.next().value) // 1622650073
ごく簡単の例ですが、ジェネレータにwhile(true){}
を利用するととても便利だと感じています。一定の範囲内でコードを生成してほしいならジェネレータ関数に引数、あるいはyield
で工夫していい。範囲のないコード生成はwhile(true){}
を使えばいい、途中で止めてほしいならreturn()
。