【Javascript基礎講座】AsyncGeneratorを正しく初期化する


※ 当ページには【広告/PR】を含む場合があります。
2024/04/18
TypeScriptのInterfaceでStaticなクラスメソッドを適用させる方法
個人的Typescriptの型入門① 〜 型の基本
蛸壺の技術ブログ|【Javascript基礎講座】AsyncGeneratorを正しく初期化する



通常のfor構文に見るイタレーターと比較して、イマイチ認知度の低い仕組みの一つに「ジェネレーター」があります。
イタレーターもジェネレーターも繰り返し処理を行う意味では似たように感じられるかもしれませんが、Javascriptの何らかのコードを書いている時に、イタレーターよりもジェネレーターの方が好ましいケースも結構あります。
基本的なジェネレーターの特徴と使い方は以前の記事で簡単に触れています。

合同会社タコスキングダム|蛸壺の技術ブログ
【Rxjs基礎講座】GeneratorをObservableへ変換する方法

RxjsでJavascript標準のGenerator関数をObservableへ変換する方法を検証します。



さらに言うと、ジェネレーターに非同期処理を行わせたい場合には、
「AsyncGenerator」 と呼ばれるビルドインオブジェクトが利用できます。

参考|AsyncGenerator

少し困るのが、AsyncGeneratorの初期化方法です。
今回はちょっとしたことですが、AsyncGeneratorについて学んでみましょう。



合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

AsyncGeneratorの基本的な使い方



まず基本的な使い方から振り返ってみます。
Javascriptから
AsyncGenerator を呼び出すには、通常のジェネレーター( function* )の呼び出しの前に async を付けて使います。

            const generator = async function*() {
    yield* await Promise.resolve([1, 2, 3]);
};

//👇await generator()とならないことに注意
const gen = generator();

console.log(await gen.next());
console.log(await gen.next());
console.log(await gen.next());
console.log(await gen.next());

        

これを実行すると、

            { value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

        

というように
next メソッドから非同期にジェネレーターの値が引き出せます。
ここで
AsyncGenerator を使う上でいくつか注意が必要なことがあります。
通常よく慣れ親しんでいる
async function という用法で返される Promise 型のインスタンスに頭が引っ張られると、どうしても const gen = await generator() のように await で受けたくなってしまいます。
ですが、
async function* から返されるのは AsyncGenerator 型のインスタンスであるため、非同期処理にはならず、そのため await で受けることができません。
「Async...」を冠する名前から少し誤解を生みそうですが、
AsyncGeneratorのインスタンスの初期化は「同期的」AsyncGeneratorからnextメソッドからPromise型で呼び出される値は「非同期的」 です。
もう一例やってみます。

AsyncGenerator のインスタンスの初期化が同期的だと理解できたら、ジェネレーターの外部から引数として適当なオブジェクトを使って初期化させることもできます。

            const generator = async function*(data) {
    yield* data;
};

const data = await Promise.resolve([1, 2, 3]);
const gen = generator(data);

console.log(await gen.next());
console.log(await gen.next());
console.log(await gen.next());
console.log(await gen.next());

        

これを実行すると先程と同じ結果が得られます。

            { value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

        

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

AsyncGeneratorをクラスのメンバ変数として初期化する



もう少し
AsyncGenerator の応用を広げて、独自定義したクラスの中でフィールドの一つとして AsyncGenerator 型を初期化したい場合を考えてみます。
関連するテーマとして以前詳しく説明していた、
「クラスコンストラクタの内部でasync/awaitをどう使うか」 でも取り上げましたが、Javascriptのコンストラクタも 「同期的」・「即時的」 な性質を持ちます。

合同会社タコスキングダム|蛸壺の技術ブログ
【javascript基礎講座】クラスコンストラクタの内部で async/await を使う時の注意

async/await構文をjavascript標準のクラス・コンストラクタで利用するための初期化のテクニックを解説



コンストラクタが"同期的"なので、
AsyncGenerator 型のメンバ変数もコンストラクタ内で初期化することができます。

            class HogeClass {
    constructor() {
        this.gen = initHogeAsyncGenerator();
    }

    initHogeAsyncGenerator = async function*() {
        yield* await Promise.resolve([1, 2, 3]);
    };
}

const hoge = new HogeClass();

console.log(await hoge.gen.next());
console.log(await hoge.gen.next());
console.log(await hoge.gen.next());
console.log(await hoge.gen.next());

        

これを実行しても先程と同じ結果が得られます。

            { value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

        

問題になってしまうのが、
AsyncGenerator 型のメンバーフィールドを、「非同期」で得られる中身で初期化したい場合です。
つまり、

            class HogeClass {
    constructor() {
        this.gen = initHogeAsyncGenerator();
        //👇コンストラクタ内部でPromise型の中身は解決できない
        const data = await Promise.resolve([1, 2, 3]);
        this.gen = initHogeAsyncGenerator(data);
    }

    initHogeAsyncGenerator = async function*(data) {
        yield* data;
    };
}

const hoge = new HogeClass();
//...

        

ということで、
AsyncGenerator 型のメンバ変数を持つクラスの初期化においては、コンストラクタは使わずにファクトリー関数で初期化させるほうが好ましい場合があります。

            class HogeClass {
    constructor() {}

    initHogeAsyncGenerator = async function*(data) {
        yield* data;
    };

    static async initHogeFactory() {
        const obj = new HogeClass();

        //👇ここで非同期に初期化させたい値を引き出す
        const lazyData = await Promise.resolve([1, 2, 3]);
        obj.gen = obj.initHogeAsyncGenerator(lazyData);

        return obj;
    }
}

const hoge = await HogeClass.initHogeFactory();

console.log(await hoge.gen.next());
console.log(await hoge.gen.next());
console.log(await hoge.gen.next());
console.log(await hoge.gen.next());

        

これを実行すると期待した結果が得られます。

            { value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

        

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

まとめ



今回は
AsyncGenerator を使うときに押さえておきたい利用上の基本的な初期化の方法をざっと解説していきました。
どこがどう同期・非同期ということを考えながらでないと、いざエラーが出てしまったときにバグが発見しにくいので、躓く前に一度じっくりと用法の基本を理解してみてください。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き