2017年5月14日日曜日

ES6で非同期ジェネレータ

正確にはasyncはES7だけど


はじめに。

ここにジェネレータを使ったコードがあるじゃろ?
function main() {
        const values = generateValues();
        for(const value of values) {
                console.log(value);
        }
}

function *generateValues() {
        const values = getValues();
        for(const value of values) {
                yield value;
        }
}

function getValues() {
        return [1, 2, 3, 4, 5];
}

main();
いやいや真ん中のgenerateValues()いらんやろ、なんで無駄にジェネレータ使っとんねんというツッコミは置いといて。

この中のgetValues()を、DBから取ってくるとかそういう理由で非同期にしたい場合は単純に考えるとこうなります。
// 非同期関数として定義!
async function main()
{
        const values = await generateValues();
        for(const value of values)
        {
                console.log(value);
        }
}

// 非同期ジェネレータ?
async function *generateValues() {
        const values = await getValues();
        for(const value of values) {
                yield value;
        }
}

// これを非同期にしたい
async function getValues() {       
        return [1, 2, 3, 4, 5];
}

main();
でも、そもそもこれは構文エラー。 関数定義にasync*は同時に使えないらしい。

asyncの内部実装はジェネレータらしいからそのへんが原因なのかもね。

asyncじゃなくてPromiseを使ってみた(→失敗)

ES6のasyncPromiseの糖衣構文なので、Promiseを使えば同時にasyncキーワードを使えそうだ!と思い立ちました。あったまいー。
// これでどうだ?
function *generateValues() {
        getValues().then((values) => {
                for(const value of values) {
                        yield value;
                }
        }
}
でも、thenのコールバック関数内からyieldはできない。そしてコールバック関数が構文エラー。

あえなく撃沈…

iterableプロトコルを自作してみる(→成功!)

次の手段として、iterableプロトコルを自作してみることに。

generateValues()がiterableプロトコルを返せばfor ofを使えるので、基本に立ち返ってみました。
generateValues()の内部でGeneratorオブジェクトを作って返却したコードがこちら。
async function generateValues() {
        const values = await getValues();
        return g();

        function *g() {
                for(const value of values) {
                        yield value;
                }
        }
}
これでうまくいった!

何が難しかったかって、「非同期 ジェネレータ」で検索しても、「ジェネレータを使った非同期処理の実装方法」ばっかりでてくるところです。

結局何がやりたいのかいまいちわからないという方は続きもお読みください。

本当の戦いはこれからだ

実は、本当にやりたいのはこれじゃなくて、generateValues()内でgetValues()値がなくなるまで何度もコールしたいんです。

これが上で無駄にジェネレータを使っていた理由です。

(構文エラーの)非同期ジェネレータで書くとこんなかんじ。
async function *generateValues() {
        while(true) {
                const values = await getValues();
                if(value === null) {
                        // もうなければおしまい
                        break;
                }
                for(const value of values) {
                        yield value;
                }
        }
}
これのどこが嬉しいかというと、getValues()がDBから次々とデータを取ってくるような実装だと、呼び出し元のmain()全件取れるまで何億件でも延々とfor of構文で回せるところ。なんと美しい。

取得部分は非同期なのでCPUを専有することはありませんし、一度に取得する件数に上限をつけておけば大量のデータを一気に取ってOOMキラーにぬっころされることもありません。

誰か解決してくれる優しくて頭のいい人いないかな…|ω・`)チラッ

Q&A

次の値が取れるまでポーリングで回せば?
ポーリング時間かかるから嫌だよ!
for of諦めてコールバック関数で受け取れば幸せになれるよ!
for ofのほうが美しいからこっち使いたい!まあどうしても無理ならコールバック使うけど…
処女厨みたいなこだわり捨てろよ。そんなだから童貞なんだよ
どどど童貞ちゃうわ!

0 件のコメント:

コメントを投稿