坂本研のゼミ室

イテレータとジェネレータ

ES2015から新たに導入された、イテレータ(iterator)とジェネレータ(generator)という概念についてまとめました。

イテレータ

イテレータ(反復子)は繰り返しのための機構で、以下の2つの種類のオブジェクトがあります。

この2つは相反するものでは無く、反復可能かつ反復子でもあるオブジェクトもあります。

反復可能なオブジェクト (iterable object)

  • Symbol.iteratorと呼ばれるプロパティを含んでいるオブジェクト、このプロパティの値はイテレータを返す関数

  • for...ofを使って各要素について処理できる (配列、文字列、Map、Set)

反復子オブジェクト(iterator object)

  • nextと呼ばれるメソッドを実装しているオブジェクト。nextメソッドはvaluedoneというプロパティをもつオブジェクトを返す

  • 通常はイテレータオブジェクトの意味でイテレータという

配列についてみていく

配列は反復可能なオブジェクト (iterable object)です。

  • 配列が反復可能なオブジェクトの例
const foodstuffs = [
    'egg',
    'milk',
    'sugar'
]

for (let afood of foodstuffs) {
    console.log(afood)
}

/*  実行結果を表示
egg
milk
sugar
*/

しかし、配列はイテレータ(反復子オブジェクト)ではありません。

イテレータは上記の通りnextというメソッドを呼び出す事で要素を取り出せるオブジェクトです。

先ほどの配列にnextを使うと配列はイテレータ(反復子オブジェクト)では無いのでエラーになる

foodstuffs.next()

/* 実行結果を表示
TypeError: foodstuffs.next is not a function
*/

しかし、valuesというメソッドを使う事で配列をイテレータ(反復子オブジェクト)に変換する事ができる

// itという変数にイテレータを代入している
const it = foodstuffs.values()

これによってイテレータを作成することができたのでnextメソッドを使って要素を取り出すことができる

nextを呼び出すとvaluedoneの2つのプロパティをもつオブジェクトが返される

  • value : 処理を行うために使う値

  • done : 全ての要素を返したかどうかを示す真偽値

作成したitnextを使ってみる

const foodstuffs = [
    'egg',
    'milk',
    'sugar'
]

const it = foodstuffs.values()

console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())

/* 実行結果を表示
{ value: 'egg', done: false }
{ value: 'milk', done: false }
{ value: 'sugar', done: false }
// 供給するデータがなくなるとvalueはundefinedに、doneはtrueになる 
{ value: undefined, done: true }
*/

イテレータは前の状態を記憶して、nextメソッドが呼ばれるたびに次のデータを供給している

イテレータプロトコル

イテレータ(反復子オブジェクト)であるためには、イテレータプロトコルを実装する必要があり、そのために、Symbol.iteratorを実装します。

これは、イテレータの振る舞いをするオブジェクトを戻すものです。つまり、nextメソッドを呼ぶとvaluedoneをもつオブジェクトを返すようなメソッドを実装するということです。

詳しくはこちらを参考にしてください

class Food {
    constructor() {
        this.foodstuffs = []
    }

    add(foodstuffs) {
        const now = Date.new()
        console.log(`食材追加: ${foodstuffs}(${now})`)
        this.foodstuffs.push({ foodstuffs, timestamp: now})
    }
    [Symbol.iterator]() {
        return this.foodstuffs.values()
    }
}
  • nextが呼ばれた時にvaluedoneをプロパティにもつオブジェクトを返すオブジェクト実装する方法
[Symbol.iterator]() {
    let i = 0;
    const foodstuffs = this.foodstuffs
    return (
        next: () => i => foodstuffs.length ? {value: undefined, done: true} : {value: foodstuffs[i++], done: false}
    )
}

つまり、最初の例のfoodstuffsは配列で、配列は反復可能なオブジェクト (iterable object)なので、[Symbol.iterator]と持っています。

メソッド[Symbol.iterator]は実行するとイテレータを返すので、配列は反復可能なオブジェクトをもとにイテレータを作成するのは簡単です。

配列のメソッド[Symbol.iterator]を実行して返ってくる値を使えば良いです。

  • 上の例をこのように書くこともできる
[Symbol.iterator]() {
    return this.foodstuffs[Symbol.iterator]()
}

ジェネレータ

ジェネレータは関数の一種と考えることができますが、ジェネレータを呼び出しても関数のようにすぐには実行されず、まずはイテレータが返されます。その後、イテレータのメソッドnextを呼び出すたびに実行が進んでいきます。

ジェネレータを定義する場合functionの後に*をつけます。また、呼び出し元に値を提供するためにはyieldを使います(returnも使われますが通常は値を返すためには使われません)。それ以外は普通の関数と同じ構文です。

ちなみにジェネレータはアロー関数のように記述はできないそうです。

  • ジェネレータの例
function* weeks() {
    yield '日'
    yield '月'
    yield '火'
    yield '水'
    yield '木'
    yield '金'
    yield '土'
}

上で作成したジェネレータは以下のように呼び出すことができます。

ジェネレータを呼び出すとイテレータが返ってくるので、そのイテレータnextメソッドを使って順に処理していきます。

const it = weeks()
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())

/* 実行結果の表示
{ value: '日', done: false }
{ value: '月', done: false }
{ value: '火', done: false }
{ value: '水', done: false }
{ value: '木', done: false }
{ value: '金', done: false }
{ value: '土', done: false }
{ value: undefined, done: true }
*/

return

ジェネレータのどこかでreturnを呼び出すとdonetrueになり、valueプロパティはreturnで指定した値になります。

ジェネレータから有用な値を提供する場合yieldを使うべきです。

yield式と双方向コミュニケーション

ジェネレータを使うと呼び出し側との間で双方向の会話が可能になります。そのためにyieldを使います。

yieldは式なので、評価の結果何かしらの値を持ちます。どんな値かというとnext呼び出し時の引数の値です。

ジェネレータは遅延評価を可能にしています。

  • 双方向の会話ができるジェネレータの例
function* conversation() {
    const name = yield '名前は?'
    const food = yield '好きな食べ物は?'
    return `${name}さんの好きな食べ物は${food}だそうです!`
}

const it = conversation()
console.log(it.next())
console.log(it.next('たろう'))
console.log(it.next('オムライス'))
console.log(it.next())

/* 実行結果の表示
{ value: '名前は?', done: false }
{ value: '好きな食べ物は?', done: false }
{ value: 'たろうさんの好きな食べ物はオムライスだそうです!', done: true }
{ value: undefined, done: true }
*/

参考資料

O'Reilly Japan - 初めてのJavaScript 第3版

Symbol.iterator - JavaScript | MDN

ES2015のIteratorとGeneratorについて - mikami's blog