2017/10/25更新

[Javascript] ES6のジェネレーターを使う

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
Javascriptでもジェネレーターが使えるようになってきました。今日は使い方をブログに書きたいと思います。



目次




サンプル実装

この記事にある実装内容は、以下にまとまっています。適宜ご参照いただけたら嬉しいです。

https://github.com/yoheiMune/frontend-playground/blob/master/021-generator/sample.js



ジェネレーターとは

Pythonなど他の言語にはある機能で、データをちょっとずつ処理できるような機能を提供します。例えば以下のように使います。
// ジェネレーター関数
function *readFiles() {
    for (let i = 0; i < 100; i++) {
        // ファイルを1つ読み込んでは、呼び出し元に返す.
        const file = readFile(i)
        yield file
    }    
}

// ジェネレーターを使う.
for (file of readFiles()) {
    // 1つずつファイルを処理する.
    consume(file)
}
上記の例では、100個のファイルを読み込んで処理する際に、ジェネレーターを使っています。ジェネレーターを使うことで1ファイルずつ処理することができます。100個全てのファイルを同時に読み込まないので、メモリに優しい実装です。

フロントエンドで大量データを扱うことは稀ですが、ジェネレーターを使えるようになっておくと、自分の引き出しが増えていいかなと思います(フロントエンドだと「ES7のasync/await」がジェネレーターのシンタックスシュガーになっています)。

具体的な実装を見ていきたいと思います。



ジェネレーターを使う

具体的な実装を見ていきたいと思います。


基本的な使い方

ジェネレーターの関数は、関数定義で*を付与して定義します。そして値を返したいところでyield構文を利用します。
// ジェネレーターを定義する
function* gen () {
    yield 1
    yield 2
    yield 3
}
上記のジェネレーターは以下のように呼び出すことができます。
// ジェネレーターを使う
var g = gen()
g.next() // { value: 1, done: false } 
g.next() // { value: 2, done: false }
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
next関数を呼び出すことで1つずつ値を取りダウことができ、返却内容のdoneから、ジェネレーターが終了したのかを知ることができます。

また、ジェネレータはfor-of構文でも利用することができます。
// ジェネレーターを使う(for-of構文)
for (item of gen()) {
    console.log(item)
}
// 1
// 2
// 3
for-of構文の場合には、値のみが返ってきます(doneは返却されない)。

これが基本的な使い方です。


ジェネレーターの中にジェネレーターを含める

ジェネレーターの中に、さらにジェネレーターを含めることができます。yield*構文を用います。
// ジェネレーターの定義
function* gen2 () {
    yield* ['a', 'b', 'c']
    yield* gen()
}

// 呼び出す
for (item of gen2()) {
    console.log(item)
}
// a
// b
// c
// 1
// 2
// 3
呼び出し側はジェネレーターの中身がどうなっているのかは気にすることなく、1つずつ値を取り出すことができます。


ジェネレーターに値を戻す

ジェネレーターの呼び出し側は、next()関数の引数に値を指定することで、ジェネレーターに値を返すことができます。
function* calc () {
    let v = 1
    while (true) {
        v = v + v
        // 返って来た値を処理する.
        reset = yield v
        if (reset) {
            v = 1
        }
    }
}
console.log('---')
var g = calc()
console.log(g.next().value)  // 2
console.log(g.next().value)  // 4
console.log(g.next().value)  // 8

// 値を返すことができる.
console.log(g.next(true).value)  // 2
console.log(g.next().value)   // 4
上記では計算結果をリセットしていますが、next関数に値を指定することで、値をジェネレーターに渡すことができます。


ジェネレーターに例外を通知する

値を返す他に、例外を送り込むこともできます。
以下の場合には、ジェネレーター内で例外処理をしているので、例外が通知されても処理は引き続き続行されます。
// ジェネレーター関数(例外処理あり)
function* myGen () {
    while (true) {
        try {
            yield 'good'
        } catch (e) {
            console.log(e.message)
        }
    }
}

var g = myGen()
console.log(g.next().value)  // good
var result = g.throw(new Error('Oh My Error...')) // Oh My Error...
console.log(result)    // { value: 'good', done: false }  <= 例外通知後も「good」で「done=false」
console.log(g.next().value)  // good

逆に以下の場合には、ジェネレーター内で例外処理をしないので、呼び出し元に例外が返って来ます。そしてジェネレーターは終了します。
// ジェネレーター関数(例外処理なし)
function* myGen () {
    while (true) {
        yield 'good'
    }
}

var g = myGen()
console.log(g.next().value)  // good
try {
    var result = g.throw(new Error('Oh My Error...'))
} catch (e) {
    // ジェネレーターが処理しないので、例外がここに入る.
    console.log(e.message) // Oh My Error...
}
console.log(g.next())  // { value: undefined, done: true }  <= 「done=true」で完了状態になる.
このあたりの機能を使うことは稀かなと思いますが・・・、お勉強ということでサンプル実装を書いてみました。



参考資料

今回の記事を書くために、以下の資料を参照しました。ありがとうございます。

- Generator - JavaScript | MDN

- イテレーターとジェネレーター - JavaScript | MDN - Iterators and generators - JavaScript | MDN

- function* - JavaScript | MDN



最後に

新しい機能を学ぶと視野も広がっていいですね。Javascriptのイテレーターも使う機会があれば使ってみようと思います。Pythonなどでは時々ジェネレーターを使っています。大量のファイルやリクエストを扱う場合に、一度にメモリに載せると非効率(またはメモリに載らない)な場合に、ジェネレーターでちょっとずつ処理しています。

本ブログでは、フロントエンド、Node.js、Go言語、Python、Linux、インフラ、Swift、Java、機械学習、などの技術トピックを発信をしていきます。「プログラミングで困ったその時に、解決の糸口を見つけられる」そんな目標でブログを書き続けています。今後も役立つネタを書いていきますので、ぜひ本ブログのRSSTwitterをフォローして貰えたら嬉しいです ^ ^

最後までご覧頂きましてありがとうございました!





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。