22. ジェネレータ
目次
本書をサポートしてください:購入 (PDF、EPUB、MOBI) または 寄付
(広告、ブロックしないでください。)

22. ジェネレータ



22.1 概要

22.1.1 ジェネレータとは何か?

ジェネレータは、一時停止と再開が可能なプロセス(コードの一部)と考えてください。

function* genFunc() {
    // (A)
    console.log('First');
    yield;
    console.log('Second');
}

新しい構文に注意してください:function*は、ジェネレータ関数ジェネレータメソッドもあります)のための新しい「キーワード」です。yieldは、ジェネレータが自身を一時停止できる演算子です。さらに、ジェネレータはyieldを介して入力の受信と出力の送信も行えます。

ジェネレータ関数genFunc()を呼び出すと、ジェネレータオブジェクトgenObjが取得され、これを使用してプロセスを制御できます。

const genObj = genFunc();

プロセスは最初はA行で一時停止されています。genObj.next()によって実行が再開され、genFunc()内のyieldによって実行が一時停止されます。

genObj.next();
// Output: First
genObj.next();
// output: Second

22.1.2 ジェネレータの種類

ジェネレータには4種類あります。

  1. ジェネレータ関数宣言
     function* genFunc() { ··· }
     const genObj = genFunc();
    
  2. ジェネレータ関数式
     const genFunc = function* () { ··· };
     const genObj = genFunc();
    
  3. オブジェクトリテラル内のジェネレータメソッド定義
     const obj = {
         * generatorMethod() {
             ···
         }
     };
     const genObj = obj.generatorMethod();
    
  4. クラス定義(クラス宣言またはクラス式)内のジェネレータメソッド定義
     class MyClass {
         * generatorMethod() {
             ···
         }
     }
     const myInst = new MyClass();
     const genObj = myInst.generatorMethod();
    

22.1.3 ユースケース:イテラブルの実装

ジェネレータによって返されるオブジェクトはイテラブルです。各yieldは、反復される値のシーケンスに貢献します。したがって、ジェネレータを使用してイテラブルを実装できます。これは、さまざまなES6言語メカニズム(for-ofループ、スプレッド演算子(...)など)によって消費できます。

次の関数は、オブジェクトのプロパティを反復処理するイテラブルを返し、プロパティごとに[キー、値]ペアを1つずつ返します。

function* objectEntries(obj) {
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        // `yield` returns a value and then pauses
        // the generator. Later, execution continues
        // where it was previously paused.
        yield [propKey, obj[propKey]];
    }
}

objectEntries()は次のように使用されます。

const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

objectEntries()がどのように動作するかについては、専用のセクションで説明されています。ジェネレータを使用せずに同じ機能を実装するには、はるかに多くの作業が必要です。

22.1.4 ユースケース:よりシンプルな非同期コード

ジェネレータを使用して、Promiseの操作を大幅に簡素化できます。Promiseベースの関数fetchJson()とそのジェネレータによる改善方法を見てみましょう。

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}

coライブラリとジェネレータを使用すると、この非同期コードは同期的に見えます。

const fetchJson = co.wrap(function* (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});

ECMAScript 2017には、内部的にジェネレータに基づいている非同期関数があります。これらを使用すると、コードは次のようになります。

async function fetchJson(url) {
    try {
        let request = await fetch(url);
        let text = await request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
}

すべてのバージョンは次のように呼び出すことができます。

fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));

22.1.5 ユースケース:非同期データの受信

ジェネレータは、yieldを介してnext()から入力を受け取ることができます。つまり、新しいデータが非同期的に到着するたびにジェネレータを起動でき、ジェネレータは同期的にデータを受け取っているように感じます。

22.2 ジェネレータとは何か?

ジェネレータは、一時停止と再開が可能な関数です(協調的マルチタスクまたはコルーチンと考えてください)。これにより、さまざまなアプリケーションが可能になります。

最初の例として、名前がgenFuncである次のジェネレータ関数を考えてみましょう。

function* genFunc() {
    // (A)
    console.log('First');
    yield; // (B)
    console.log('Second'); // (C)
}

2つの点が、genFuncを通常の関数宣言と区別します。

genFuncを呼び出しても、その本体は実行されません。代わりに、いわゆるジェネレータオブジェクトが取得され、これを使用して本体の実行を制御できます。

> const genObj = genFunc();

genFunc()は、最初は本体の前(A行)で中断されています。メソッド呼び出しgenObj.next()は、次のyieldまで実行を続けます。

> genObj.next()
First
{ value: undefined, done: false }

最終行でわかるように、genObj.next()もオブジェクトを返します。今はそれを無視しましょう。後で重要になります。

genFuncは現在B行で一時停止しています。もう一度next()を呼び出すと、実行が再開され、C行が実行されます。

> genObj.next()
Second
{ value: undefined, done: true }

その後、関数は終了し、実行は本体から抜け出し、genObj.next()のさらなる呼び出しは影響を与えません。

22.2.1 ジェネレータの役割

ジェネレータは3つの役割を果たすことができます。

  1. イテレータ(データ生成元):各yieldnext()を介して値を返すことができます。つまり、ジェネレータはループと再帰を介して値のシーケンスを生成できます。ジェネレータオブジェクトがIterableインターフェースを実装しているため(イテレーションに関する章で説明されています)、これらのシーケンスは、イテラブルをサポートするES6構成によって処理できます。2つの例としては、for-ofループとスプレッド演算子(...)があります。
  2. オブザーバ(データ消費元):yieldnext()(パラメータを介して)からも値を受け取ることができます。つまり、ジェネレータは、next()を介して新しい値がプッシュされるまで一時停止するデータ消費元になります。
  3. コルーチン(データ生成元と消費元):ジェネレータは一時停止可能であり、データ生成元とデータ消費元の両方であるため、コルーチン(協調的にマルチタスク化されたタスク)に変換するために多くの作業は必要ありません。

次のセクションでは、これらの役割について詳しく説明します。

22.3 ジェネレータとしてイテレータ (データ生成)

前述のように、ジェネレータオブジェクトは、データ生成元、データ消費元、またはその両方になることができます。このセクションでは、データ生成元としてのジェネレータについて説明します。ジェネレータは、IterableIteratorの両方のインターフェースを実装しています(以下に示されています)。つまり、ジェネレータ関数の結果は、イテラブルとイテレータの両方です。ジェネレータオブジェクトの完全なインターフェースについては、後で説明します。

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

Iterableインターフェースのreturn()メソッドは、このセクションでは関連しないため省略しました。

ジェネレータ関数は、yieldを介して値のシーケンスを生成し、データ消費元はイテレータメソッドnext()を介してこれらの値を消費します。たとえば、次のジェネレータ関数は'a''b'の値を生成します。

function* genFunc() {
    yield 'a';
    yield 'b';
}

この相互作用は、ジェネレータオブジェクトgenObjを介して生成された値を取得する方法を示しています。

> const genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }
> genObj.next()
{ value: 'b', done: false }
> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }

22.3.1 ジェネレータを反復処理する方法

ジェネレータオブジェクトはイテラブルであるため、イテラブルをサポートするES6言語構成を適用できます。特に重要なのは次の3つです。

まず、for-ofループ

for (const x of genFunc()) {
    console.log(x);
}
// Output:
// a
// b

次に、スプレッド演算子(...)です。これは、反復されたシーケンスを配列の要素に変換します(この演算子の詳細については、パラメータ処理に関する章を参照してください)。

const arr = [...genFunc()]; // ['a', 'b']

最後に、デストラクチャリング

> const [x, y] = genFunc();
> x
'a'
> y
'b'

22.3.2 ジェネレータからの返却

前のジェネレータ関数には、明示的なreturnが含まれていませんでした。暗黙的なreturnは、undefinedを返すことと同等です。明示的なreturnを含むジェネレータを調べましょう。

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'result';
}

返された値は、next()によって返される最後のオブジェクト(そのプロパティdonetrueです)に表示されます。

> const genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }

ただし、イテラブルで動作するほとんどの構成は、doneオブジェクト内の値を無視します。

for (const x of genFuncWithReturn()) {
    console.log(x);
}
// Output:
// a
// b

const arr = [...genFuncWithReturn()]; // ['a', 'b']

再帰的なジェネレータ呼び出しを行うための演算子であるyield*は、doneオブジェクト内の値を考慮します。これは後で説明します。

22.3.3 ジェネレータからの例外の送出

例外がジェネレータの本体から抜けると、next()はその例外を送出します。

function* genFunc() {
    throw new Error('Problem!');
}
const genObj = genFunc();
genObj.next(); // Error: Problem!

つまり、next()は3種類の異なる「結果」を生成できます。

22.3.4 例:プロパティの反復処理

ジェネレータがイテラブルの実装にいかに便利であるかを示す例を見てみましょう。次の関数objectEntries()は、オブジェクトのプロパティを反復処理するイテラブルを返します。

function* objectEntries(obj) {
    // In ES6, you can use strings or symbols as property keys,
    // Reflect.ownKeys() retrieves both
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        yield [propKey, obj[propKey]];
    }
}

この関数は、for-ofループを使用してオブジェクトjaneのプロパティを反復処理できるようにします。

const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

比較のために – ジェネレータを使用しないobjectEntries()の実装ははるかに複雑です。

function objectEntries(obj) {
    let index = 0;
    let propKeys = Reflect.ownKeys(obj);

    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (index < propKeys.length) {
                let key = propKeys[index];
                index++;
                return { value: [key, obj[key]] };
            } else {
                return { done: true };
            }
        }
    };
}

22.3.5 ジェネレータ内でのみyieldを使用できます

ジェネレータの大きな制限の1つは、ジェネレータ関数内(静的に)にある場合にのみyieldを使用できることです。つまり、コールバック内でyieldを使用することはできません。

function* genFunc() {
    ['a', 'b'].forEach(x => yield x); // SyntaxError
}

yieldは、ジェネレータ関数以外では使用できません。そのため、前のコードは構文エラーを引き起こします。この場合、コールバックを使用しないようにコードを書き直すのは簡単です(下記参照)。しかし、残念ながら常にそれが可能なわけではありません。

function* genFunc() {
    for (const x of ['a', 'b']) {
        yield x; // OK
    }
}

この制限の利点は後で説明します。ジェネレータの実装を容易にし、イベントループとの互換性を高めます。

22.3.6 yield*による再帰

yieldはジェネレータ関数内でのみ使用できます。したがって、ジェネレータで再帰アルゴリズムを実装する場合は、あるジェネレータから別のジェネレータを呼び出す方法が必要です。このセクションでは、それが思っているよりも複雑であることを示します。そのため、ES6にはこれ専用の演算子yield*があります。現時点では、両方のジェネレータが出力する場合のyield*の動作のみを説明します。入力に関与する場合の動作については後で説明します。

あるジェネレータが別のジェネレータを再帰的に呼び出すにはどうすればよいでしょうか?ジェネレータ関数fooを作成したと仮定します。

function* foo() {
    yield 'a';
    yield 'b';
}

別のジェネレータ関数barからfooを呼び出すにはどうすればよいでしょうか?次の方法は機能しません!

function* bar() {
    yield 'x';
    foo(); // does nothing!
    yield 'y';
}

foo()を呼び出すとオブジェクトが返されますが、foo()は実際には実行されません。そのため、ECMAScript 6には再帰的なジェネレータ呼び出しを行うための演算子yield*があります。

function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}

// Collect all values yielded by bar() in an array
const arr = [...bar()];
    // ['x', 'a', 'b', 'y']

内部的には、yield*はおおよそ次のように動作します。

function* bar() {
    yield 'x';
    for (const value of foo()) {
        yield value;
    }
    yield 'y';
}

yield*のオペランドはジェネレータオブジェクトである必要はなく、任意の反復可能オブジェクトにすることができます。

function* bla() {
    yield 'sequence';
    yield* ['of', 'yielded'];
    yield 'values';
}

const arr = [...bla()];
    // ['sequence', 'of', 'yielded', 'values']
22.3.6.1 yield*は反復終了値を考慮します

反復可能オブジェクトをサポートするほとんどの構成要素は、反復終了オブジェクト(プロパティdonetrue)に含まれる値を無視します。ジェネレータはreturnを介してその値を提供します。yield*の結果は、反復終了値です。

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'The result';
}
function* logReturned(genObj) {
    const result = yield* genObj;
    console.log(result); // (A)
}

A行に到達するには、まずlogReturned()によって生成されたすべての値を反復処理する必要があります。

> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]
22.3.6.2 ツリーの反復処理

再帰によるツリーの反復処理は簡単ですが、従来の方法でツリーのイテレータを作成するのは複雑です。そのため、ジェネレータはここで威力を発揮します。ジェネレータを使用すると、再帰を使用してイテレータを実装できます。例として、二分木に関する次のデータ構造を考えてみましょう。これは、キーがSymbol.iteratorであるメソッドを持っているため、反復可能です。そのメソッドはジェネレータメソッドであり、呼び出されるとイテレータを返します。

class BinaryTree {
    constructor(value, left=null, right=null) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    /** Prefix iteration */
    * [Symbol.iterator]() {
        yield this.value;
        if (this.left) {
            yield* this.left;
            // Short for: yield* this.left[Symbol.iterator]()
        }
        if (this.right) {
            yield* this.right;
        }
    }
}

次のコードは二分木を作成し、for-ofを使用してそれを反復処理します。

const tree = new BinaryTree('a',
    new BinaryTree('b',
        new BinaryTree('c'),
        new BinaryTree('d')),
    new BinaryTree('e'));

for (const x of tree) {
    console.log(x);
}
// Output:
// a
// b
// c
// d
// e

22.4 オブザーバとしてのジェネレータ(データ消費)

データのコンシューマとして、ジェネレータオブジェクトはジェネレータインターフェースの後半、Observerに準拠します。

interface Observer {
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

オブザーバとして、ジェネレータは入力を受け取るまで一時停止します。インターフェースで指定されたメソッドを介して送信される3種類の入力があります。

22.4.1 next()による値の送信

ジェネレータをオブザーバとして使用する場合は、next()を介して値を送信し、yieldを介してそれらの値を受け取ります。

function* dataConsumer() {
    console.log('Started');
    console.log(`1. ${yield}`); // (A)
    console.log(`2. ${yield}`);
    return 'result';
}

このジェネレータを対話的に使用してみましょう。まず、ジェネレータオブジェクトを作成します。

> const genObj = dataConsumer();

ここでgenObj.next()を呼び出すと、ジェネレータが開始されます。実行は最初のyieldまで続きます。そこでジェネレータは一時停止します。next()の結果はA行で生成された値です(yieldにオペランドがないためundefined)。このセクションでは、値を取得するためではなく値を送信するためだけに使用するため、next()が返すものには関心がありません。

> genObj.next()
Started
{ value: undefined, done: false }

最初のyieldに値'a'を、2番目のyieldに値'b'を送信するために、next()をさらに2回呼び出します。

> genObj.next('a')
1. a
{ value: undefined, done: false }

> genObj.next('b')
2. b
{ value: 'result', done: true }

最後のnext()の結果は、dataConsumer()から返された値です。donetrueであることは、ジェネレータが終了したことを示します。

残念ながら、next()は非対称ですが、それは避けられません。常に現在中断されているyieldに値を送信しますが、次のyieldのオペランドを返します。

22.4.1.1 最初のnext()

ジェネレータをオブザーバとして使用する場合、最初のnext()呼び出しの目的はオブザーバを開始することだけであることに注意することが重要です。この最初の呼び出しによって実行が最初のyieldに進められるため、その後にのみ入力が可能になります。したがって、最初のnext()を介して送信する入力は無視されます。

function* gen() {
    // (A)
    while (true) {
        const input = yield; // (B)
        console.log(input);
    }
}
const obj = gen();
obj.next('a');
obj.next('b');

// Output:
// b

最初は、A行で実行が一時停止します。最初のnext()呼び出しは

2回目のnext()呼び出しは

次のユーティリティ関数は、この問題を解決します。

/**
 * Returns a function that, when called,
 * returns a generator object that is immediately
 * ready for input via `next()`
 */
function coroutine(generatorFunction) {
    return function (...args) {
        const generatorObject = generatorFunction(...args);
        generatorObject.next();
        return generatorObject;
    };
}

coroutine()の動作を確認するために、ラップされたジェネレータと通常のジェネレータを比較してみましょう。

const wrapped = coroutine(function* () {
    console.log(`First input: ${yield}`);
    return 'DONE';
});
const normal = function* () {
    console.log(`First input: ${yield}`);
    return 'DONE';
};

ラップされたジェネレータは、すぐに入力の準備ができます。

> wrapped().next('hello!')
First input: hello!

通常のジェネレータは、入力の準備ができるまで、追加のnext()が必要です。

> const genObj = normal();
> genObj.next()
{ value: undefined, done: false }
> genObj.next('hello!')
First input: hello!
{ value: 'DONE', done: true }

22.4.2 yieldはゆるくバインドされます

yieldは非常にゆるくバインドされるため、オペランドを括弧で囲む必要はありません。

yield a + b + c;

これは次のように扱われます。

yield (a + b + c);

次のようにではありません。

(yield a) + b + c;

その結果、多くの演算子はyieldよりも強くバインドされるため、それをオペランドとして使用したい場合は、yieldを括弧で囲む必要があります。たとえば、括弧なしのyieldをプラスのオペランドにすると、SyntaxErrorが発生します。

console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK

yieldが関数またはメソッド呼び出しの直接の引数である場合は、括弧は必要ありません。

foo(yield 'a', yield 'b');

代入の右辺でyieldを使用する場合も、括弧は必要ありません。

const input = yield;
22.4.2.1 ES6構文におけるyield

yieldの周りの括弧の必要性は、ECMAScript 6仕様の次の構文規則に見ることができます。これらの規則は、式がどのように解析されるかを記述しています。ここでは、一般的(ゆるいバインディング、低い優先順位)から具体的(強いバインディング、高い優先順位)の順にリストアップします。特定の種類の式が要求される場合は、より具体的な式を使用することもできます。逆は当てはまりません。階層はParenthesizedExpressionで終わります。つまり、括弧で囲めば、どこにでも任意の式を指定できます。

Expression :
    AssignmentExpression
    Expression , AssignmentExpression
AssignmentExpression :
    ConditionalExpression
    YieldExpression
    ArrowFunction
    LeftHandSideExpression = AssignmentExpression
    LeftHandSideExpression AssignmentOperator AssignmentExpression

···

AdditiveExpression :
    MultiplicativeExpression
    AdditiveExpression + MultiplicativeExpression
    AdditiveExpression - MultiplicativeExpression
MultiplicativeExpression :
    UnaryExpression
    MultiplicativeExpression MultiplicativeOperator UnaryExpression

···

PrimaryExpression :
    this
    IdentifierReference
    Literal
    ArrayLiteral
    ObjectLiteral
    FunctionExpression
    ClassExpression
    GeneratorExpression
    RegularExpressionLiteral
    TemplateLiteral
    ParenthesizedExpression
ParenthesizedExpression :
    ( Expression )

AdditiveExpressionのオペランドは、AdditiveExpressionMultiplicativeExpressionです。したがって、オペランドとして(より具体的な)ParenthesizedExpressionを使用することは問題ありませんが、(より一般的な)YieldExpressionを使用することはできません。

22.4.3 return()throw()

ジェネレータオブジェクトには、next()と同様の追加メソッドreturn()throw()が2つあります。

next(x)の動作(最初の呼び出し後)を再確認してみましょう。

  1. ジェネレータは現在、yield演算子で一時停止しています。
  2. そのyieldに値xを送信します。つまり、xとして評価されます。
  3. 次のyieldreturn、またはthrowに進みます。
    • yield xは、next(){ value: x, done: false }を返すことになります。
    • return xは、next(){ value: x, done: true }を返すことになります。
    • throw err(ジェネレータ内でキャッチされない)は、next()errをスローすることになります。

return()throw()next()と同様に動作しますが、ステップ2で異なることを行います。

22.4.4 return()はジェネレータを終了します

return()は、ジェネレータの最後の中断につながったyieldの位置でreturnを実行します。その動作を確認するために、次のジェネレータ関数を使用してみましょう。

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } finally {
        console.log('Exiting');
    }
}

次のやり取りでは、まずnext()を使用してジェネレータを開始し、A行のyieldまで進めます。次に、return()を使用してその位置から返します。

> const genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.return('Result')
Exiting
{ value: 'Result', done: true }
22.4.4.1 終了の防止

finally句内でyieldを使用すると(その句でreturnステートメントを使用することも可能です)、return()によるジェネレータの終了を防ぐことができます。

function* genFunc2() {
    try {
        console.log('Started');
        yield;
    } finally {
        yield 'Not done, yet!';
    }
}

今回は、return()はジェネレータ関数を終了しません。したがって、それが返すオブジェクトのプロパティdonefalseです。

> const genObj2 = genFunc2();

> genObj2.next()
Started
{ value: undefined, done: false }

> genObj2.return('Result')
{ value: 'Not done, yet!', done: false }

next()をもう1回呼び出すことができます。ジェネレータ関数以外の関数と同様に、ジェネレータ関数の戻り値は、finally句に入る前にキューに入れられた値です。

> genObj2.next()
{ value: 'Result', done: true }
22.4.4.2 新規ジェネレータからの戻り値

まだ開始されていない新規ジェネレータからの値の返却は許可されます。

> function* genFunc() {}
> genFunc().return('yes')
{ value: 'yes', done: true }

22.4.5 throw()はエラーを知らせます

throw() は、ジェネレータの最後のサスペンドを引き起こした yield の位置で例外をスローします。ジェネレータ関数を用いて、その動作を調べましょう。

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } catch (error) {
        console.log('Caught: ' + error);
    }
}

次のやり取りでは、まず next() を使用してジェネレータを開始し、A行の yield まで実行します。その後、その位置から例外をスローします。

> const genObj1 = genFunc1();

> genObj1.next()
Started
{ value: undefined, done: false }

> genObj1.throw(new Error('Problem!'))
Caught: Error: Problem!
{ value: undefined, done: true }

throw() の結果(最終行に示されている)は、暗黙的な return で関数を抜けたことによるものです。

22.4.5.1 新規ジェネレータからのスロー

まだ開始されていない新規ジェネレータで例外をスローすることは許可されています。

> function* genFunc() {}
> genFunc().throw(new Error('Problem!'))
Error: Problem!

22.4.6 例:非同期でプッシュされたデータの処理

オブザーバとしてのジェネレータは入力待ちの間一時停止するため、非同期で受信されるデータのオンデマンド処理に最適です。ジェネレータのチェーンを設定するためのパターンは次のとおりです。

チェーン全体は、非同期リクエストを行い、next() を介してジェネレータのチェーンに結果をプッシュする、ジェネレータではない関数によって先頭に付けられています。

例として、非同期で読み取られるファイルを処理するためのジェネレータをチェーンしてみましょう。

次のコードはチェーンを設定します。ジェネレータ splitLinesnumberLinesprintLinesが含まれています。データは、ジェネレータではない関数 readFile を介してチェーンにプッシュされます。

readFile(fileName, splitLines(numberLines(printLines())));

これらの関数の動作については、コードを示した際に説明します。

前述のように、ジェネレータが yield を介して入力を受け取る場合、ジェネレータオブジェクトに対する next() の最初の呼び出しは何もしません。そのため、ここでは先に示したヘルパー関数 coroutine() を使用してコルーチンを作成します。これは、最初の next() を実行します。

readFile() は、すべてを開始するジェネレータではない関数です。

import {createReadStream} from 'fs';

/**
 * Creates an asynchronous ReadStream for the file whose name
 * is `fileName` and feeds it to the generator object `target`.
 *
 * @see ReadStream https://node.dokyumento.jp/api/fs.html#fs_class_fs_readstream
 */
function readFile(fileName, target) {
    const readStream = createReadStream(fileName,
        { encoding: 'utf8', bufferSize: 1024 });
    readStream.on('data', buffer => {
        const str = buffer.toString('utf8');
        target.next(str);
    });
    readStream.on('end', () => {
        // Signal end of output sequence
        target.return();
    });
}

ジェネレータのチェーンは splitLines で始まります。

/**
 * Turns a sequence of text chunks into a sequence of lines
 * (where lines are separated by newlines)
 */
const splitLines = coroutine(function* (target) {
    let previous = '';
    try {
        while (true) {
            previous += yield;
            let eolIndex;
            while ((eolIndex = previous.indexOf('\n')) >= 0) {
                const line = previous.slice(0, eolIndex);
                target.next(line);
                previous = previous.slice(eolIndex+1);
            }
        }
    } finally {
        // Handle the end of the input sequence
        // (signaled via `return()`)
        if (previous.length > 0) {
            target.next(previous);
        }
        // Signal end of output sequence
        target.return();
    }
});

重要なパターンに注意してください。

次のジェネレータは numberLines です。

//**
 * Prefixes numbers to a sequence of lines
 */
const numberLines = coroutine(function* (target) {
    try {
        for (const lineNo = 0; ; lineNo++) {
            const line = yield;
            target.next(`${lineNo}: ${line}`);
        }
    } finally {
        // Signal end of output sequence
        target.return();
    }
});

最後のジェネレータは printLines です。

/**
 * Receives a sequence of lines (without newlines)
 * and logs them (adding newlines).
 */
const printLines = coroutine(function* () {
    while (true) {
        const line = yield;
        console.log(line);
    }
});

このコードの良い点は、すべてが遅延して(オンデマンドで)発生することです。行は、到着するにつれて分割、番号付け、印刷されます。すべてのテキストが到着するまで待たなくても、印刷を開始できます。

22.4.7 yield*:全体像

大まかな経験則として、yield* は、あるジェネレータ(呼び出し元)から別のジェネレータ(被呼び出し元)への関数呼び出し(と同等のもの)を実行します。

これまで、yield の1つの側面しか見ていませんでした。それは、生成された値を被呼び出し元から呼び出し元に伝播することです。入力を受け取るジェネレータに関心を持つようになった今、別の側面が重要になります。yield* は、呼び出し元が受け取った入力を被呼び出し元にも転送します。ある意味、被呼び出し元がアクティブなジェネレータになり、呼び出し元のジェネレータオブジェクトを介して制御できます。

22.4.7.1 例:yield* による next() の転送

次のジェネレータ関数 caller() は、yield* を介してジェネレータ関数 callee() を呼び出します。

function* callee() {
    console.log('callee: ' + (yield));
}
function* caller() {
    while (true) {
        yield* callee();
    }
}

callee は、next() を介して受け取った値をログに記録します。これにより、caller に送信した値 'a''b' を受け取るかを確認できます。

> const callerObj = caller();

> callerObj.next() // start
{ value: undefined, done: false }

> callerObj.next('a')
callee: a
{ value: undefined, done: false }

> callerObj.next('b')
callee: b
{ value: undefined, done: false }

throw()return() も同様に転送されます。

22.4.7.2 JavaScript で表現された yield* のセマンティクス

JavaScript でどのように実装するかを示すことで、yield* の完全なセマンティクスを説明します。

次のステートメント

let yieldStarResult = yield* calleeFunc();

は、おおよそ次と同等です。

let yieldStarResult;

const calleeObj = calleeFunc();
let prevReceived = undefined;
while (true) {
    try {
        // Forward input previously received
        const {value,done} = calleeObj.next(prevReceived);
        if (done) {
            yieldStarResult = value;
            break;
        }
        prevReceived = yield value;
    } catch (e) {
        // Pretend `return` can be caught like an exception
        if (e instanceof Return) {
            // Forward input received via return()
            calleeObj.return(e.returnedValue);
            return e.returnedValue; // “re-throw”
        } else {
            // Forward input received via throw()
            calleeObj.throw(e); // may throw
        }
    }
}

単純にするために、このコードにはいくつかのものが欠けています。

22.5 コルーチンとしてのジェネレータ(協調的マルチタスク)

ジェネレータは、データのソースまたはシンクとして使用されるのを見てきました。多くのアプリケーションでは、これらの2つの役割を厳密に分離することが良い習慣です。なぜなら、物事をよりシンプルに保つことができるからです。このセクションでは、ジェネレータの完全なインターフェース(両方の役割を組み合わせたもの)と、両方の役割が必要になる1つのユースケースについて説明します。それは、タスクが情報を送受信できる必要がある協調的マルチタスクです。

22.5.1 完全なジェネレータインターフェース

ジェネレータオブジェクトGeneratorの完全なインターフェースは、出力と入力を両方処理します。

interface Generator {
    next(value? : any) : IteratorResult;
    throw(value? : any) : IteratorResult;
    return(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

インターフェースGeneratorは、これまで見てきた2つのインターフェース、出力用のIteratorと入力用のObserverを組み合わせたものです。

interface Iterator { // data producer
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

22.5.2 協調的マルチタスク

協調的マルチタスクは、出力と入力を両方処理する必要があるジェネレータのアプリケーションです。その仕組みに入る前に、JavaScriptにおける並列処理の現状を確認しましょう。

JavaScriptは単一プロセスで動作します。この制限を解消する方法は2つあります。

2つのユースケースが、協調的マルチタスクから恩恵を受けます。なぜなら、それらは、ほとんどシーケンシャルな制御フローを伴い、時折一時停止するからです。

22.5.2.1 ジェネレータによる非同期計算の簡素化

いくつかの Promise ベースのライブラリは、ジェネレータを使用して非同期コードを簡素化します。ジェネレータは、結果が到着するまで一時停止できるため、Promises のクライアントとして理想的です。

次の例は、T.J. Holowaychuk によるライブラリcoを使用した場合の動作を示しています。2つのライブラリが必要です(babel-node を介して Node.js コードを実行する場合)。

import fetch from 'isomorphic-fetch';
const co = require('co');

co は協調的マルチタスクのための実際のライブラリであり、isomorphic-fetch は新しい Promise ベースの fetch API(XMLHttpRequest の代替です。「That’s so fetch!」をJake Archibald の記事で詳細を確認してください)のポリフィルです。fetch を使用すると、Promise を介して url のファイルのテキストを返す getFile 関数を簡単に記述できます。

function getFile(url) {
    return fetch(url)
        .then(request => request.text());
}

これで、co を使用するためのすべての準備が整いました。次のタスクは、2つのファイルのテキストを読み取り、その中の JSON を解析し、結果をログに記録します。

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

A行で非同期呼び出しを行っているにもかかわらず、このコードがいかに同期的に見えるかに注目してください。タスクとしてのジェネレータは、スケジューラ関数 co に Promise を yield することで非同期呼び出しを行います。yield はジェネレータを一時停止します。Promise が結果を返すと、スケジューラは next() を介して結果を渡すことでジェネレータを再開します。co の簡単なバージョンを以下に示します。

function co(genFunc) {
    const genObj = genFunc();
    step(genObj.next());

    function step({value,done}) {
        if (!done) {
            // A Promise was yielded
            value
            .then(result => {
                step(genObj.next(result)); // (A)
            })
            .catch(error => {
                step(genObj.throw(error)); // (B)
            });
        }
    }
}

next()(A行)と throw()(B行)は、例外をスローする可能性があることを無視しました(例外がジェネレータ関数の本体からエスケープしたとき)。

22.5.3 ジェネレータによる協調的マルチタスクの限界

コルーチンは協調的マルチタスク処理を行うタスクであり、制限がありません。コルーチン内では、任意の関数がコルーチン全体(関数のアクティブ化自体、関数呼び出し元のアクティブ化、呼び出し元の呼び出し元など)を中断できます。

対照的に、ジェネレータはジェネレータ内部から直接しか中断できず、中断されるのは現在の関数アクティブ化のみです。これらの制限により、ジェネレータは時々浅いコルーチン [3]と呼ばれます。

22.5.3.1 ジェネレータの制限による利点

ジェネレータの制限には、主に2つの利点があります。

JavaScriptには、すでに非常に単純なスタイルの協調的マルチタスク処理があります。それは、タスクのキューでの実行をスケジュールするイベントループです。各タスクは関数を呼び出すことで開始され、その関数が完了すると終了します。イベント、setTimeout()、その他のメカニズムによって、タスクがキューに追加されます。

このマルチタスク処理のスタイルは、実行完了という重要な保証を行います。すべての関数は、完了するまで別のタスクによって中断されないという点に依存できます。関数はトランザクションになり、中間状態にあるデータを見られることなく、完全なアルゴリズムを実行できます。共有データへの同時アクセスはマルチタスクを複雑にし、JavaScriptの同時実行モデルでは許可されていません。そのため、実行完了は良いことです。

しかし、コルーチンは実行完了を妨げます。なぜなら、任意の関数が呼び出し元を中断できるからです。たとえば、次のアルゴリズムは複数のステップで構成されています。

step1(sharedData);
step2(sharedData);
lastStep(sharedData);

step2がアルゴリズムを中断した場合、アルゴリズムの最後のステップが実行される前に、他のタスクが実行される可能性があります。これらのタスクには、アプリケーションの他の部分を含めることができ、それらはsharedDataを未完成の状態で見ます。ジェネレータは実行完了を維持し、自分自身のみを中断して呼び出し元に返ります。

coおよび同様のライブラリは、コルーチンの欠点なしに、そのほとんどの機能を提供します。

22.6 ジェネレータの例

このセクションでは、ジェネレータの使用方法の例をいくつか示します。

22.6.1 ジェネレータによる反復可能オブジェクトの実装

反復処理に関する章では、いくつかの反復可能オブジェクトを手動で実装しました。このセクションでは、代わりにジェネレータを使用します。

22.6.1.1 反復可能オブジェクトコンビネータtake()

take()は、(潜在的に無限の)反復値のシーケンスを、長さnのシーケンスに変換します。

function* take(n, iterable) {
    for (const x of iterable) {
        if (n <= 0) return;
        n--;
        yield x;
    }
}

使用例を以下に示します。

const arr = ['a', 'b', 'c', 'd'];
for (const x of take(2, arr)) {
    console.log(x);
}
// Output:
// a
// b

ジェネレータを使用しないtake()の実装は、より複雑です。

function take(n, iterable) {
    const iter = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (n > 0) {
                n--;
                return iter.next();
            } else {
                maybeCloseIterator(iter);
                return { done: true };
            }
        },
        return() {
            n = 0;
            maybeCloseIterator(iter);
        }
    };
}
function maybeCloseIterator(iterator) {
    if (typeof iterator.return === 'function') {
        iterator.return();
    }
}

反復可能オブジェクトコンビネータzip()は、複数の反復可能オブジェクトが関与しており、for-ofを使用できないため、ジェネレータを介して実装することによる利点はほとんどありません。

22.6.1.2 無限反復可能オブジェクト

naturalNumbers()は、すべての自然数の反復可能オブジェクトを返します。

function* naturalNumbers() {
    for (let n=0;; n++) {
        yield n;
    }
}

この関数は、多くの場合、コンビネータと組み合わせて使用されます。

for (const x of take(3, naturalNumbers())) {
    console.log(x);
}
// Output
// 0
// 1
// 2

非ジェネレータ実装を以下に示しますので、比較してください。

function naturalNumbers() {
    let n = 0;
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            return { value: n++ };
        }
    }
}
22.6.1.3 配列にヒントを得た反復可能オブジェクトコンビネータ:mapfilter

配列は、mapおよびfilterメソッドを使用して変換できます。これらのメソッドは、入力を反復可能オブジェクトとし、出力を反復可能オブジェクトとするように一般化できます。

22.6.1.3.1 一般化されたmap()

これは、mapの一般化されたバージョンです。

function* map(iterable, mapFunc) {
    for (const x of iterable) {
        yield mapFunc(x);
    }
}

map()は無限反復可能オブジェクトで機能します。

> [...take(4, map(naturalNumbers(), x => x * x))]
[ 0, 1, 4, 9 ]
22.6.1.3.2 一般化されたfilter()

これは、filterの一般化されたバージョンです。

function* filter(iterable, filterFunc) {
    for (const x of iterable) {
        if (filterFunc(x)) {
            yield x;
        }
    }
}

filter()は無限反復可能オブジェクトで機能します。

> [...take(4, filter(naturalNumbers(), x => (x % 2) === 0))]
[ 0, 2, 4, 6 ]

22.6.2 遅延評価のためのジェネレータ

次の2つの例では、ジェネレータを使用して文字ストリームを処理する方法を示します。

巧妙な点は、すべてが遅延(増分的に、オンデマンドで)計算されることです。計算は最初の文字が到着するとすぐに開始されます。たとえば、最初の単語を取得するために、すべての文字が到着するまで待つ必要はありません。

22.6.2.1 遅延プル(イテレータとしてのジェネレータ)

ジェネレータを使用した遅延プルは、次のように機能します。ステップ1〜3を実装する3つのジェネレータは、次のように連結されます。

addNumbers(extractNumbers(tokenize(CHARS)))

各チェーンメンバーはソースからデータを取得し、アイテムのシーケンスを生成します。処理は、ソースが文字列CHARSであるtokenizeから開始されます。

22.6.2.1.1 ステップ1 - トークン化

次のトリックにより、コードが少しシンプルになります。シーケンスの終了イテレータの結果(プロパティdonefalseである)は、センテンス値END_OF_SEQUENCEに変換されます。

/**
 * Returns an iterable that transforms the input sequence
 * of characters into an output sequence of words.
 */
function* tokenize(chars) {
    const iterator = chars[Symbol.iterator]();
    let ch;
    do {
        ch = getNextItem(iterator); // (A)
        if (isWordChar(ch)) {
            let word = '';
            do {
                word += ch;
                ch = getNextItem(iterator); // (B)
            } while (isWordChar(ch));
            yield word; // (C)
        }
        // Ignore all other characters
    } while (ch !== END_OF_SEQUENCE);
}
const END_OF_SEQUENCE = Symbol();
function getNextItem(iterator) {
    const {value,done} = iterator.next();
    return done ? END_OF_SEQUENCE : value;
}
function isWordChar(ch) {
    return typeof ch === 'string' && /^[A-Za-z0-9]$/.test(ch);
}

このジェネレータはどのように遅延しているのでしょうか?next()を介してトークンを要求すると、トークンを生成するために必要な回数だけそのiterator(A行とB行)を取得し、そのトークンを生成します(C行)。その後、再度トークンを要求されるまで一時停止します。つまり、トークン化は最初の文字が利用可能になるとすぐに開始されるため、ストリームに便利です。

トークン化を試してみましょう。スペースとドットは単語ではないことに注意してください。これらは無視されますが、単語を区切ります。文字列は文字(Unicodeコードポイント)の反復可能オブジェクトであるという事実を使用します。tokenize()の結果は単語の反復可能オブジェクトであり、スプレッド演算子(...)を使用して配列に変換します。

> [...tokenize('2 apples and 5 oranges.')]
[ '2', 'apples', 'and', '5', 'oranges' ]
22.6.2.1.2 ステップ2 - 数字の抽出

このステップは比較的シンプルで、数字のみを含む単語をNumber()を介して数値に変換した後にyieldします。

/**
 * Returns an iterable that filters the input sequence
 * of words and only yields those that are numbers.
 */
function* extractNumbers(words) {
    for (const word of words) {
        if (/^[0-9]+$/.test(word)) {
            yield Number(word);
        }
    }
}

遅延性も確認できます。next()を介して数値を要求すると、wordsで数値が検出されるとすぐに(yieldを介して)1つ取得します。

単語の配列から数値を抽出して見ましょう。

> [...extractNumbers(['hello', '123', 'world', '45'])]
[ 123, 45 ]

文字列は数値に変換されることに注意してください。

22.6.2.1.3 ステップ3 - 数字の加算
/**
 * Returns an iterable that contains, for each number in
 * `numbers`, the total sum of numbers encountered so far.
 * For example: 7, 4, -1 --> 7, 11, 10
 */
function* addNumbers(numbers) {
    let result = 0;
    for (const n of numbers) {
        result += n;
        yield result;
    }
}

簡単な例を試してみましょう。

> [...addNumbers([5, -2, 12])]
[ 5, 3, 15 ]
22.6.2.1.4 出力のプル

ジェネレータのチェーン自体は、出力を作成しません。スプレッド演算子を介して出力を積極的にプルする必要があります。

const CHARS = '2 apples and 5 oranges.';
const CHAIN = addNumbers(extractNumbers(tokenize(CHARS)));
console.log([...CHAIN]);
    // [ 2, 7 ]

ヘルパー関数logAndYieldを使用すると、ものが実際に遅延して計算されているかどうかを調べることができます。

function* logAndYield(iterable, prefix='') {
    for (const item of iterable) {
        console.log(prefix + item);
        yield item;
    }
}

const CHAIN2 = logAndYield(addNumbers(extractNumbers(tokenize(logAndYield(CHA\
RS)))), '-> ');
[...CHAIN2];

// Output:
// 2
//  
// -> 2
// a
// p
// p
// l
// e
// s
//  
// a
// n
// d
//  
// 5
//  
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .

出力は、addNumbersが文字'2'' 'が受信されるとすぐに結果を生成することを示しています。

22.6.2.2 遅延プッシュ(オブザーバブルとしてのジェネレータ)

以前のプルベースのアルゴリズムをプッシュベースのアルゴリズムに変換するために必要な作業はほとんどありません。手順は同じです。ただし、プルで終了する代わりに、プッシュで開始します。

前述のように、ジェネレータが yield を介して入力を受け取る場合、ジェネレータオブジェクトに対する next() の最初の呼び出しは何もしません。そのため、ここでは先に示したヘルパー関数 coroutine() を使用してコルーチンを作成します。これは、最初の next() を実行します。

次の関数send()はプッシュを実行します。

/**
 * Pushes the items of `iterable` into `sink`, a generator.
 * It uses the generator method `next()` to do so.
 */
function send(iterable, sink) {
    for (const x of iterable) {
        sink.next(x);
    }
    sink.return(); // signal end of stream
}

ジェネレータがストリームを処理する場合、ストリームの終了を認識して適切にクリーンアップする必要があります。プルでは、特別なストリーム終了センテンスを使用してこれを行いました。プッシュでは、ストリームの終了はreturn()を介してシグナルされます。

受信したすべてのものを単純に出力するジェネレータを介してsend()をテストしてみましょう。

/**
 * This generator logs everything that it receives via `next()`.
 */
const logItems = coroutine(function* () {
    try {
        while (true) {
            const item = yield; // receive item via `next()`
            console.log(item);
        }
    } finally {
        console.log('DONE');
    }
});

文字列(Unicodeコードポイントの反復可能オブジェクト)を介してlogItems()に3文字を送信してみましょう。

> send('abc', logItems());
a
b
c
DONE
22.6.2.2.1 ステップ1 - トークン化

このジェネレータが2つのfinally句でストリームの終了(return()を介してシグナルされる)にどのように反応するかを見てください。A行で始まる無限ループは終了しないため、ジェネレータが終了しないように、2つのyieldのいずれかにreturn()が送信されることを依存しています。

/**
 * Receives a sequence of characters (via the generator object
 * method `next()`), groups them into words and pushes them
 * into the generator `sink`.
 */
const tokenize = coroutine(function* (sink) {
    try {
        while (true) { // (A)
            let ch = yield; // (B)
            if (isWordChar(ch)) {
                // A word has started
                let word = '';
                try {
                    do {
                        word += ch;
                        ch = yield; // (C)
                    } while (isWordChar(ch));
                } finally {
                    // The word is finished.
                    // We get here if
                    // - the loop terminates normally
                    // - the loop is terminated via `return()` in line C
                    sink.next(word); // (D)
                }
            }
            // Ignore all other characters
        }
    } finally {
        // We only get here if the infinite loop is terminated
        // via `return()` (in line B or C).
        // Forward `return()` to `sink` so that it is also
        // aware of the end of stream.
        sink.return();
    }
});

function isWordChar(ch) {
    return /^[A-Za-z0-9]$/.test(ch);
}

今回は、遅延性はプッシュによって駆動されます。ジェネレータが単語に必要な数の文字を受信するとすぐに(C行)、単語をsinkにプッシュします(D行)。つまり、ジェネレータはすべての文字を受信するまで待ちません。

tokenize() は、ジェネレータが線形状態マシンの実装としてうまく機能することを示しています。この場合、マシンには「単語内」と「単語外」の2つの状態があります。

文字列をトークン化してみましょう。

> send('2 apples and 5 oranges.', tokenize(logItems()));
2
apples
and
5
oranges
22.6.2.2.2 ステップ2 - 数字の抽出

このステップは簡単です。

/**
 * Receives a sequence of strings (via the generator object
 * method `next()`) and pushes only those strings to the generator
 * `sink` that are “numbers” (consist only of decimal digits).
 */
const extractNumbers = coroutine(function* (sink) {
    try {
        while (true) {
            const word = yield;
            if (/^[0-9]+$/.test(word)) {
                sink.next(Number(word));
            }
        }
    } finally {
        // Only reached via `return()`, forward.
        sink.return();
    }
});

これも遅延処理です。数字が見つかり次第、sinkにプッシュされます。

単語の配列から数値を抽出して見ましょう。

> send(['hello', '123', 'world', '45'], extractNumbers(logItems()));
123
45
DONE

入力は文字列のシーケンスですが、出力は数値のシーケンスであることに注意してください。

22.6.2.2.3 ステップ3 - 数字の追加

今回は、ストリームの終わりに反応して単一の値をプッシュし、その後sinkを閉じます。

/**
 * Receives a sequence of numbers (via the generator object
 * method `next()`). For each number, it pushes the total sum
 * so far to the generator `sink`.
 */
const addNumbers = coroutine(function* (sink) {
    let sum = 0;
    try {
        while (true) {
            sum += yield;
            sink.next(sum);
        }
    } finally {
        // We received an end-of-stream
        sink.return(); // signal end of stream
    }
});

このジェネレータを試してみましょう。

> send([5, -2, 12], addNumbers(logItems()));
5
3
15
DONE
22.6.2.2.4 入力のプッシュ

ジェネレータのチェーンはtokenizeで始まり、すべて受信したものをログ出力するlogItemsで終わります。sendを使って文字のシーケンスをチェーンにプッシュします。

const INPUT = '2 apples and 5 oranges.';
const CHAIN = tokenize(extractNumbers(addNumbers(logItems())));
send(INPUT, CHAIN);

// Output
// 2
// 7
// DONE

次のコードは、処理が実際に遅延して行われることを証明します。

const CHAIN2 = tokenize(extractNumbers(addNumbers(logItems({ prefix: '-> ' })\
)));
send(INPUT, CHAIN2, { log: true });

// Output
// 2
//  
// -> 2
// a
// p
// p
// l
// e
// s
//  
// a
// n
// d
//  
// 5
//  
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .
// DONE

出力は、文字'2'' 'がプッシュされるとすぐにaddNumbersが結果を生成することを示しています。

22.6.3 ジェネレータによる協調的マルチタスク

22.6.3.1 長時間実行タスクの一時停止

この例では、ウェブページに表示されるカウンタを作成します。メインスレッドとユーザーインターフェースをブロックしない協調的なマルチタスクバージョンになるまで、初期バージョンを改良します。

これは、カウンタを表示するウェブページの部分です。

<body>
    Counter: <span id="counter"></span>
</body>

この関数は、永遠にカウントアップするカウンタを表示します5

function countUp(start = 0) {
    const counterSpan = document.querySelector('#counter');
    while (true) {
        counterSpan.textContent = String(start);
        start++;
    }
}

この関数を実行すると、実行されているユーザーインターフェーススレッドが完全にブロックされ、そのタブは応答しなくなります。

yield(このジェネレータを実行するためのスケジューリング関数は後で示します)を使用して定期的に一時停止するジェネレータを使用して、同じ機能を実装してみましょう。

function* countUp(start = 0) {
    const counterSpan = document.querySelector('#counter');
    while (true) {
        counterSpan.textContent = String(start);
        start++;
        yield; // pause
    }
}

小さな改良を加えましょう。ユーザーインターフェースの更新を別のジェネレータdisplayCounterに移動し、yield*で呼び出します。ジェネレータなので、一時停止も処理できます。

function* countUp(start = 0) {
    while (true) {
        start++;
        yield* displayCounter(start);
    }
}
function* displayCounter(counter) {
    const counterSpan = document.querySelector('#counter');
    counterSpan.textContent = String(counter);
    yield; // pause
}

最後に、countUp()を実行するために使用できるスケジューリング関数です。ジェネレータの実行ステップはそれぞれ、setTimeout()で作成される個別のタスクによって処理されます。つまり、ユーザーインターフェースは間に他のタスクをスケジュールでき、応答性を維持します。

function run(generatorObject) {
    if (!generatorObject.next().done) {
        // Add a new task to the event queue
        setTimeout(function () {
            run(generatorObject);
        }, 1000);
    }
}

runを使うことで、ユーザーインターフェースをブロックしない(ほぼ)無限のカウントアップを実現できます。

run(countUp());
22.6.3.2 ジェネレータとNode.jsスタイルのコールバックによる協調的マルチタスク

ジェネレータ関数(またはメソッド)を呼び出す場合、そのジェネレータオブジェクトにアクセスできません。そのthisは、ジェネレータ関数ではない通常の関数だった場合のthisと同じです。回避策として、yieldを介してジェネレータオブジェクトをジェネレータ関数に渡します。

次のNode.jsスクリプトはこのテクニックを使用しますが、ジェネレータオブジェクトをコールバック(next、A行)でラップします。babel-nodeで実行する必要があります。

import {readFile} from 'fs';

const fileNames = process.argv.slice(2);

run(function* () {
    const next = yield;
    for (const f of fileNames) {
        const contents = yield readFile(f, { encoding: 'utf8' }, next);
        console.log('##### ' + f);
        console.log(contents);
    }
});

A行では、Node.jsのコールバック規約に従う関数で使用できるコールバックを取得します。コールバックは、run()の実装に見られるように、ジェネレータオブジェクトを使用してジェネレータをウェイクアップします。

function run(generatorFunction) {
    const generatorObject = generatorFunction();

    // Step 1: Proceed to first `yield`
    generatorObject.next();

    // Step 2: Pass in a function that the generator can use as a callback
    function nextFunction(error, result) {
        if (error) {
            generatorObject.throw(error);
        } else {
            generatorObject.next(result);
        }
    }
    generatorObject.next(nextFunction);

    // Subsequent invocations of `next()` are triggered by `nextFunction`
}
22.6.3.3 通信シーケンシャルプロセス(CSP)

ライブラリjs-cspは、JavaScriptに通信シーケンシャルプロセス(CSP)をもたらします。これは、ClojureScriptのcore.asyncやGoのgoroutinesと同様の協調的マルチタスクのスタイルです。js-cspには2つの抽象化があります。

例として、関数型リアクティブプログラミングを彷彿とさせる方法で、CSPを使用してDOMイベントを処理してみましょう。次のコードは、listen()関数(後で示します)を使用して、mousemoveイベントを出力するチャネルを作成します。その後、無限ループ内でtakeを使用して、継続的に出力を取得します。yieldのおかげで、チャネルが出力するまでプロセスはブロックされます。

import csp from 'js-csp';

csp.go(function* () {
    const element = document.querySelector('#uiElement1');
    const channel = listen(element, 'mousemove');
    while (true) {
        const event = yield csp.take(channel);
        const x = event.layerX || event.clientX;
        const y = event.layerY || event.clientY;
        element.textContent = `${x}, ${y}`;
    }
});

listen()は次のように実装されています。

function listen(element, type) {
    const channel = csp.chan();
    element.addEventListener(type,
        event => {
            csp.putAsync(channel, event);
        });
    return channel;
}

22.7 反復API(ジェネレータを含む)内の継承

これは、ECMAScript 6でさまざまなオブジェクトがどのように接続されているかの図です(Allen Wirf-Brockの図に基づいています)。

凡例

この図から2つの興味深い事実がわかります。

まず、ジェネレータ関数gはコンストラクタと非常によく似ています(ただし、newで呼び出すことはできません。TypeErrorが発生します)。作成するジェネレータオブジェクトは、そのインスタンスであり、g.prototypeに追加されたメソッドはプロトタイプメソッドになります。

> function* g() {}
> g.prototype.hello = function () { return 'hi!'};
> const obj = g();
> obj instanceof g
true
> obj.hello()
'hi!'

第二に、すべてのジェネレータオブジェクトで使用可能なメソッドを作成する場合は、(Generator).prototypeに追加するのが最適です。そのオブジェクトにアクセスする方法は次のとおりです。

const Generator = Object.getPrototypeOf(function* () {});
Generator.prototype.hello = function () { return 'hi!'};
const generatorObject = (function* () {})();
generatorObject.hello(); // 'hi!'

22.7.1 IteratorPrototype

図には(Iterator)はありません。そのようなオブジェクトは存在しないためです。しかし、instanceofの動作と、(IteratorPrototype)g1()のプロトタイプであることを考えると、g1()Iteratorのインスタンスであると言うこともできます。

ES6のすべてのイテレータは、プロトタイプチェーンに(IteratorPrototype)を持っています。そのオブジェクトは、次のメソッドを持っているため反復可能です。したがって、すべてのES6イテレータは反復可能です(その結果、for-ofなどを適用できます)。

[Symbol.iterator]() {
    return this;
}

仕様では、(IteratorPrototype)にアクセスするために次のコードを使用することを推奨しています。

const proto = Object.getPrototypeOf.bind(Object);
const IteratorPrototype = proto(proto([][Symbol.iterator]()));

次のように使用することもできます。

const IteratorPrototype = proto(proto(function* () {}.prototype));

ECMAScript 6仕様の引用

ECMAScriptコードは、IteratorPrototypeから継承するオブジェクトを定義することもできます。IteratorPrototypeオブジェクトは、すべてのイテレータオブジェクトに適用できる追加のメソッドを追加できる場所を提供します。

IteratorPrototypeは、今後のECMAScriptのバージョンで直接アクセス可能になり、map()filter()などのツールメソッドを含むようになるでしょう(ソース)。

22.7.2 ジェネレータにおけるthisの値

ジェネレータ関数は、2つの懸念事項を組み合わせたものです。

  1. ジェネレータオブジェクトを設定して返す関数です。
  2. ジェネレータオブジェクトがステップ実行するコードが含まれています。

そのため、ジェネレータ内のthisの値がどうなるかは、すぐに明らかではありません。

関数呼び出しとメソッド呼び出しでは、thisは、gen()がジェネレータ関数ではなく通常の関数だった場合と同じになります。

function* gen() {
    'use strict'; // just in case
    yield this;
}

// Retrieve the yielded value via destructuring
const [functionThis] = gen();
console.log(functionThis); // undefined

const obj = { method: gen };
const [methodThis] = obj.method();
console.log(methodThis === obj); // true

newを介して呼び出されたジェネレータでthisにアクセスすると、ReferenceErrorが発生します(ソース:ES6仕様)。

function* gen() {
    console.log(this); // ReferenceError
}
new gen();

回避策として、ジェネレータを通常の関数でラップし、next()を介してジェネレータにジェネレータオブジェクトを渡します。つまり、ジェネレータは最初のyieldを使用してジェネレータオブジェクトを取得する必要があります。

const generatorObject = yield;

22.8 スタイルに関する考慮事項:アスタリスクの前後の空白

アスタリスクのフォーマットに関する妥当で合法的なバリエーションは次のとおりです。

これらのバリエーションのうち、どの構成でどのバリエーションが意味があり、なぜそうなのかを検討しましょう。

22.8.1 ジェネレータ関数宣言と式

ここでは、generator(または同様のもの)がキーワードとして使用できないため、アスタリスクのみが使用されています。もしそうであれば、ジェネレータ関数宣言は次のようになります。

generator foo(x, y) {
    ···
}

generatorの代わりに、ECMAScript 6はfunctionキーワードにアスタリスクを付けます。したがって、function*generatorの同義語と見なすことができ、ジェネレータ関数宣言を次のように記述することを示唆しています。

function* foo(x, y) {
    ···
}

匿名ジェネレータ関数式は、次のようにフォーマットされます。

const foo = function* (x, y) {
    ···
}

22.8.2 ジェネレータメソッド定義

ジェネレータメソッド定義を記述する際には、アスタリスクを次のようにフォーマットすることをお勧めします。

const obj = {
    * generatorMethod(x, y) {
        ···
    }
};

アスタリスクの後にスペースを付けることを支持する3つの理由があります。

第一に、アスタリスクはメソッド名の一部ではありません。一方で、ジェネレータ関数名の一部ではありません。他方で、アスタリスクはジェネレータを定義する場合にのみ言及され、使用する場合には言及されません。

第二に、ジェネレータメソッド定義は次の構文の略記です。(私の主張を明確にするために、関数式にも冗長に名前を付けています)。

const obj = {
    generatorMethod: function* generatorMethod(x, y) {
        ···
    }
};

メソッド定義がfunctionキーワードを省略することについてであれば、アスタリスクの後にスペースを付ける必要があります。

第三に、ジェネレータメソッド定義は、ゲッターとセッター(ECMAScript 5ですでに使用可能)と構文的に似ています。

const obj = {
    get foo() {
        ···
    }
    set foo(value) {
        ···
    }
};

キーワードgetsetは、通常のメソッド定義の修飾子と見なすことができます。おそらく、アスタリスクもそのような修飾子です。

22.8.3 再帰的なyieldのフォーマット

以下は、独自の生成値を再帰的に生成するジェネレータ関数の例です。

function* foo(x) {
    ···
    yield* foo(x - 1);
    ···
}

アスタリスクは異なる種類のyield演算子をマークしており、そのため上記の書き方が理にかなっています。

22.8.4 ジェネレータ関数とメソッドの文書化

Kyle Simpson (@getify)は興味深い提案をしました。Math.max()などの関数やメソッドについて記述する際に、しばしば括弧を付けることを考えると、ジェネレータ関数やメソッドについて記述する際にアスタリスクを付けるのは理にかなっているのではないでしょうか?例えば、前のセクションのジェネレータ関数を指すために*foo()と書くべきでしょうか?それに対して反対論を展開しましょう。

反復可能なオブジェクトを返す関数を記述する場合、ジェネレータはいくつかの選択肢のうちの1つに過ぎません。関数名でこの実装の詳細を明らかにしない方が良いと考えます。

さらに、ジェネレータ関数を呼び出す際にアスタリスクを使用することはありませんが、括弧は使用します。

最後に、アスタリスクは有用な情報を提供しません – yield*は、反復可能なオブジェクトを返す関数でも使用できます。しかし、反復可能なオブジェクトを返す関数やメソッド(ジェネレータを含む)の名前をマークすることは理にかなうかもしれません。例えば、サフィックスIterを使用するなどです。

22.9 FAQ:ジェネレータ

22.9.1 ジェネレータにfunction*キーワードを使用し、generatorを使用しないのはなぜですか?

下位互換性のために、generatorキーワードを使用することはできませんでした。例えば、次のコード(仮説的なES6匿名ジェネレータ式)は、ES5の関数呼び出しとコードブロックの後に続く可能性があります。

generator (a, b, c) {
    ···
}

アスタリスクによる命名スキームはyield*にもうまく拡張できると考えています。

22.9.2 yieldはキーワードですか?

yieldは厳格モードでのみ予約語です。それをES6の非厳格モードに取り込むために、トリックが使用されます。それはコンテキストキーワードになり、ジェネレータ内でのみ使用できるようになります。

22.10 結論

この章が、ジェネレータが有用で多用途なツールであることを納得させられたことを願っています。

ジェネレータによって、非同期関数呼び出しを行う際にブロックしながら、協調的にマルチタスク化されたタスクを実装できる点が気に入っています。私の意見では、それは非同期呼び出しの正しいメンタルモデルです。将来的にJavaScriptがこの方向にさらに進むことを願っています。

22.11 参考資料

この章の情報源

[1] Jafar Husainによる「Async Generator Proposal

[2] David Beazleyによる「A Curious Course on Coroutines and Concurrency

[3] David Hermanによる「Why coroutines won’t work on the web

次へ:V 標準ライブラリ