3
0

More than 1 year has passed since last update.

JavaScriptのgeneratorについて part1

Last updated at Posted at 2022-08-19

初めに

今回はジェネレータについてまとめていきたいと思います。

今回の参考文章はこちらです。
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 でジェネレータを実行する場合は、throwreturnに遭ったら完全に停止されます。
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()

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0