yield を介して値を設定しますyield はジェネレーター関数を一時停止しますyield は実行を一時停止するのですか?yield* を介してジェネレーターを呼び出す同期ジェネレーターは、常に同期イテラブルを返す、特殊なバージョンの関数定義およびメソッド定義です。
// Generator function declaration
function* genFunc1() { /*···*/ }
// Generator function expression
const genFunc2 = function* () { /*···*/ };
// Generator method definition in an object literal
const obj = {
* generatorMethod() {
// ···
}
};
// Generator method definition in a class definition
// (class declaration or class expression)
class MyClass {
* generatorMethod() {
// ···
}
}アスタリスク (*) は、関数とメソッドをジェネレーターとしてマークします。
function* は、キーワード function とアスタリスクの組み合わせです。* は (static や get と同様の) 修飾子です。yield を介して値を設定しますジェネレーター関数を呼び出すと、イテラブル (実際には、イテラブルでもあるイテレーター) が返されます。ジェネレーターは、yield 演算子を介してそのイテラブルに値を設定します。
function* genFunc1() {
yield 'a';
yield 'b';
}
const iterable = genFunc1();
// Convert the iterable to an Array, to check what’s inside:
assert.deepEqual(
Array.from(iterable), ['a', 'b']
);
// We can also use a for-of loop
for (const x of genFunc1()) {
console.log(x);
}
// Output:
// 'a'
// 'b'yield はジェネレーター関数を一時停止しますジェネレーター関数の使用には、次の手順が含まれます
iter (これはイテラブルでもあります) が返されます。iter を反復処理すると、iter.next() が繰り返し呼び出されます。毎回、値が返される yield があるまで、ジェネレーター関数の本体にジャンプします。したがって、yield はイテラブルに値を追加するだけでなく、ジェネレーター関数を一時停止して終了させます。
return と同様に、yield は関数の本体を終了し、(.next() への/経由で) 値を返します。return とは異なり、呼び出し (.next() の) を繰り返すと、実行は yield の直後から再開されます。次のジェネレーター関数を使用して、それが何を意味するのかを調べてみましょう。
let location = 0;
function* genFunc2() {
location = 1; yield 'a';
location = 2; yield 'b';
location = 3;
}genFunc2() を使用するには、まずイテレーター/イテラブル iter を作成する必要があります。genFunc2() は現在、本体の「前」で一時停止しています。
const iter = genFunc2();
// genFunc2() is now paused “before” its body:
assert.equal(location, 0);iter は イテレーションプロトコル を実装しています。したがって、iter.next() を介して genFunc2() の実行を制御します。そのメソッドを呼び出すと、一時停止した genFunc2() が再開され、yield があるまで実行されます。次に、実行が一時停止し、.next() が yield のオペランドを返します。
assert.deepEqual(
iter.next(), {value: 'a', done: false});
// genFunc2() is now paused directly after the first `yield`:
assert.equal(location, 1);'a' という yield された値はオブジェクトでラップされていることに注意してください。これが、イテレーターが常に値を配信する方法です。
iter.next() を再度呼び出すと、以前に一時停止した場所から実行が継続されます。2 番目の yield が検出されると、genFunc2() が一時停止し、.next() は yield された値 'b' を返します。
assert.deepEqual(
iter.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the second `yield`:
assert.equal(location, 2);iter.next() をもう一度呼び出すと、実行は genFunc2() の本体を離れるまで継続されます。
assert.deepEqual(
iter.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);今回は、.next() の結果のプロパティ .done が true になります。これは、イテレーターが終了したことを意味します。
yield は実行を一時停止するのですか?yield が実行を一時停止することの利点は何ですか?なぜ、配列のメソッド .push() のように、一時停止することなく値をイテラブルに設定するだけではいけないのでしょうか?
一時停止により、ジェネレーターはコルーチン (協調的にマルチタスクされるプロセスと考えてください) の多くの機能を提供します。たとえば、イテラブルの次の値を要求すると、その値は (要求に応じて) 遅延して計算されます。次の 2 つのジェネレーター関数は、それが何を意味するのかを示しています。
/**
* Returns an iterable over lines
*/
function* genLines() {
yield 'A line';
yield 'Another line';
yield 'Last line';
}
/**
* Input: iterable over lines
* Output: iterable over numbered lines
*/
function* numberLines(lineIterable) {
let lineNumber = 1;
for (const line of lineIterable) { // input
yield lineNumber + ': ' + line; // output
lineNumber++;
}
}numberLines() の yield は for-of ループ内に表示されることに注意してください。yield はループ内で使用できますが、コールバック内では使用できません (詳細については後述)。
両方のジェネレーターを組み合わせて、イテラブル numberedLines を生成してみましょう。
const numberedLines = numberLines(genLines());
assert.deepEqual(
numberedLines.next(), {value: '1: A line', done: false});
assert.deepEqual(
numberedLines.next(), {value: '2: Another line', done: false});ここでジェネレーターを使用する主な利点は、すべてが段階的に機能することです。numberedLines.next() を介して、numberLines() に 1 行の番号付き行のみを要求します。次に、genLines() に 1 行の番号なし行のみを要求します。
この漸進主義は、たとえば、genLines() が大きなテキストファイルから行を読み取る場合でも機能し続けます。numberLines() に番号付き行を要求すると、genLines() がテキストファイルから最初の行を読み取るとすぐに 1 つ取得できます。
ジェネレーターがない場合、genLines() は最初にすべての行を読み取ってから返します。次に、numberLines() はすべての行に番号を付けてから返します。したがって、最初の番号付き行を取得するまでには、はるかに長く待つ必要があります。
演習:通常の関数をジェネレーターに変える
exercises/sync-generators/fib_seq_test.mjs
次の関数 mapIter() は、配列のメソッド .map() に似ていますが、配列ではなくイテラブルを返し、結果をオンデマンドで生成します。
function* mapIter(iterable, func) {
let index = 0;
for (const x of iterable) {
yield func(x, index);
index++;
}
}
const iterable = mapIter(['a', 'b'], x => x + x);
assert.deepEqual(
Array.from(iterable), ['aa', 'bb']
); 演習:イテラブルのフィルタリング
exercises/sync-generators/filter_iter_gen_test.mjs
yield* を介してジェネレーターを呼び出すyield はジェネレーター内でのみ直接機能します。これまでのところ、別の関数またはメソッドへの yield の委任方法はありませんでした。
まず、何が機能しないかを見てみましょう。次の例では、foo() が bar() を呼び出し、後者が前者に対して 2 つの値を yield することを期待しています。残念ながら、単純なアプローチは失敗します。
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
// Nothing happens if we call `bar()`:
bar();
}
assert.deepEqual(
Array.from(foo()), []
);なぜこれは機能しないのですか?関数呼び出し bar() は、無視するイテラブルを返します。
必要なのは、foo() が bar() によって yield されるものをすべて yield することです。それが yield* 演算子の役割です。
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
yield* bar();
}
assert.deepEqual(
Array.from(foo()), ['a', 'b']
);つまり、前の foo() はほぼ次のものと同等です。
function* foo() {
for (const x of bar()) {
yield x;
}
}yield* は任意のイテラブルで機能することに注意してください。
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
Array.from(gen()), [1, 2]
);yield* を使用すると、ジェネレーターで再帰呼び出しを行うことができます。これは、ツリーなどの再帰的なデータ構造を反復処理する場合に便利です。例として、二分木の次のデータ構造を見てみましょう。
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
/** Prefix iteration: parent before children */
* [Symbol.iterator]() {
yield this.value;
if (this.left) {
// Same as yield* this.left[Symbol.iterator]()
yield* this.left;
}
if (this.right) {
yield* this.right;
}
}
}メソッド [Symbol.iterator]() はイテレーションプロトコルのサポートを追加します。つまり、for-of ループを使用して BinaryTree のインスタンスを反復処理できます。
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' 演習:ネストされた配列の反復処理
exercises/sync-generators/iter_nested_arrays_test.mjs
次のセクション の準備として、オブジェクトの「内部」にある値を反復処理する 2 つの異なるスタイルについて学ぶ必要があります。
外部イテレーション (プル):コードはイテレーションプロトコルを介してオブジェクトに値を要求します。たとえば、for-of ループは JavaScript のイテレーションプロトコルに基づいています。
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'内部イテレーション (プッシュ):オブジェクトのメソッドにコールバック関数を渡し、メソッドはコールバックに値を供給します。たとえば、配列にはメソッド .forEach() があります。
['a', 'b'].forEach((x) => {
console.log(x);
});
// Output:
// 'a'
// 'b'次のセクション には、両方のスタイルのイテレーションの例があります。
ジェネレーターの重要なユースケースの 1 つは、トラバーサルを抽出して再利用することです。
例として、ファイルのツリーをトラバースしてパスをログに記録する次の関数を考えてみましょう (これには、Node.js API を使用します)。
function logPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
console.log(filePath);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
logPaths(filePath); // recursive call
}
}
}次のディレクトリを考えてみましょう。
mydir/
a.txt
b.txt
subdir/
c.txt
mydir/ 内のパスをログに記録しましょう。
logPaths('mydir');
// Output:
// 'mydir/a.txt'
// 'mydir/b.txt'
// 'mydir/subdir'
// 'mydir/subdir/c.txt'このトラバーサルを再利用して、パスのログ記録以外に何かを行うにはどうすればよいでしょうか?
トラバーサルコードを再利用する 1 つの方法は、内部イテレーションを使用することです。トラバースされた各値は、コールバックに渡されます (行 A)。
function visitPaths(dir, callback) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
callback(filePath); // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
visitPaths(filePath, callback);
}
}
}
const paths = [];
visitPaths('mydir', p => paths.push(p));
assert.deepEqual(
paths,
[
'mydir/a.txt',
'mydir/b.txt',
'mydir/subdir',
'mydir/subdir/c.txt',
]);トラバーサルコードを再利用する別の方法は、外部イテレーションを使用することです。トラバースされたすべての値を yield するジェネレーターを作成できます (行 A)。
function* iterPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
yield filePath; // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* iterPaths(filePath);
}
}
}
const paths = Array.from(iterPaths('mydir'));Exploring ES6 のジェネレーターに関する章では、本書の範囲を超える 2 つの機能について説明しています。
yield は、.next() の引数を介してデータを受信することもできます。yield するだけでなく) return することもできます。このような値はイテレーション値になりませんが、yield* を介して取得できます。