yield
のみ使用可能yield*
による再帰next()
による値の送信yield
のゆるいバインディングreturn()
とthrow()
return()
によるジェネレータの終了throw()
によるエラーの通知yield*
:完全な解説IteratorPrototype
this
の値yield
のフォーマットfunction*
キーワードを使用し、generator
を使用しないのはなぜですか?yield
はキーワードですか?ジェネレータは、一時停止と再開が可能なプロセス(コードの一部)と考えてください。
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
ジェネレータには4種類あります。
function
*
genFunc
()
{
···
}
const
genObj
=
genFunc
();
const
genFunc
=
function
*
()
{
···
};
const
genObj
=
genFunc
();
const
obj
=
{
*
generatorMethod
()
{
···
}
};
const
genObj
=
obj
.
generatorMethod
();
class
MyClass
{
*
generatorMethod
()
{
···
}
}
const
myInst
=
new
MyClass
();
const
genObj
=
myInst
.
generatorMethod
();
ジェネレータによって返されるオブジェクトはイテラブルです。各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()
がどのように動作するかについては、専用のセクションで説明されています。ジェネレータを使用せずに同じ機能を実装するには、はるかに多くの作業が必要です。
ジェネレータを使用して、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
));
ジェネレータは、yield
を介してnext()
から入力を受け取ることができます。つまり、新しいデータが非同期的に到着するたびにジェネレータを起動でき、ジェネレータは同期的にデータを受け取っているように感じます。
ジェネレータは、一時停止と再開が可能な関数です(協調的マルチタスクまたはコルーチンと考えてください)。これにより、さまざまなアプリケーションが可能になります。
最初の例として、名前がgenFunc
である次のジェネレータ関数を考えてみましょう。
function
*
genFunc
()
{
// (A)
console
.
log
(
'First'
);
yield
;
// (B)
console
.
log
(
'Second'
);
// (C)
}
2つの点が、genFunc
を通常の関数宣言と区別します。
function*
で始まります。yield
(B行)を介して自身を一時停止できます。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()
のさらなる呼び出しは影響を与えません。
ジェネレータは3つの役割を果たすことができます。
yield
はnext()
を介して値を返すことができます。つまり、ジェネレータはループと再帰を介して値のシーケンスを生成できます。ジェネレータオブジェクトがIterable
インターフェースを実装しているため(イテレーションに関する章で説明されています)、これらのシーケンスは、イテラブルをサポートするES6構成によって処理できます。2つの例としては、for-of
ループとスプレッド演算子(...
)があります。yield
はnext()
(パラメータを介して)からも値を受け取ることができます。つまり、ジェネレータは、next()
を介して新しい値がプッシュされるまで一時停止するデータ消費元になります。次のセクションでは、これらの役割について詳しく説明します。
前述のように、ジェネレータオブジェクトは、データ生成元、データ消費元、またはその両方になることができます。このセクションでは、データ生成元としてのジェネレータについて説明します。ジェネレータは、Iterable
とIterator
の両方のインターフェースを実装しています(以下に示されています)。つまり、ジェネレータ関数の結果は、イテラブルとイテレータの両方です。ジェネレータオブジェクトの完全なインターフェースについては、後で説明します。
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 }
ジェネレータオブジェクトはイテラブルであるため、イテラブルをサポートする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'
前のジェネレータ関数には、明示的なreturn
が含まれていませんでした。暗黙的なreturn
は、undefined
を返すことと同等です。明示的なreturn
を含むジェネレータを調べましょう。
function
*
genFuncWithReturn
()
{
yield
'a'
;
yield
'b'
;
return
'result'
;
}
返された値は、next()
によって返される最後のオブジェクト(そのプロパティdone
はtrue
です)に表示されます。
> 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
オブジェクト内の値を考慮します。これは後で説明します。
例外がジェネレータの本体から抜けると、next()
はその例外を送出します。
function
*
genFunc
()
{
throw
new
Error
(
'Problem!'
);
}
const
genObj
=
genFunc
();
genObj
.
next
();
// Error: Problem!
つまり、next()
は3種類の異なる「結果」を生成できます。
x
の場合、{ value: x, done: false }
を返します。z
を持つ反復シーケンスの最後の場合、{ value: z, done: true }
を返します。ジェネレータがイテラブルの実装にいかに便利であるかを示す例を見てみましょう。次の関数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
};
}
}
};
}
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
}
}
この制限の利点は後で説明します。ジェネレータの実装を容易にし、イベントループとの互換性を高めます。
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']
yield*
は反復終了値を考慮します 反復可能オブジェクトをサポートするほとんどの構成要素は、反復終了オブジェクト(プロパティdone
がtrue
)に含まれる値を無視します。ジェネレータは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' ]
再帰によるツリーの反復処理は簡単ですが、従来の方法でツリーのイテレータを作成するのは複雑です。そのため、ジェネレータはここで威力を発揮します。ジェネレータを使用すると、再帰を使用してイテレータを実装できます。例として、二分木に関する次のデータ構造を考えてみましょう。これは、キーが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
データのコンシューマとして、ジェネレータオブジェクトはジェネレータインターフェースの後半、Observer
に準拠します。
interface
Observer
{
next
(
value
?
:
any
)
:
void
;
return
(
value
?
:
any
)
:
void
;
throw
(
error
)
:
void
;
}
オブザーバとして、ジェネレータは入力を受け取るまで一時停止します。インターフェースで指定されたメソッドを介して送信される3種類の入力があります。
next()
は通常の入力を送信します。return()
はジェネレータを終了します。throw()
はエラーを知らせます。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()
から返された値です。done
がtrue
であることは、ジェネレータが終了したことを示します。
残念ながら、next()
は非対称ですが、それは避けられません。常に現在中断されているyield
に値を送信しますが、次のyield
のオペランドを返します。
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()
呼び出しは
next()
の引数'a'
をジェネレータに供給しますが、ジェネレータはそれを受信する手段がありません(yield
がないため)。そのため、無視されます。yield
に進み、実行を一時停止します。yield
のオペランド(オペランドがないためundefined
)を返します。2回目のnext()
呼び出しは
next()
の引数'b'
をジェネレータに供給します。ジェネレータはB行のyield
を介してそれを受信し、変数input
に代入します。next()
はそのyield
のオペランド(undefined
)を返します。次のユーティリティ関数は、この問題を解決します。
/**
* 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 }
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
;
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
のオペランドは、AdditiveExpression
とMultiplicativeExpression
です。したがって、オペランドとして(より具体的な)ParenthesizedExpression
を使用することは問題ありませんが、(より一般的な)YieldExpression
を使用することはできません。
return()
とthrow()
ジェネレータオブジェクトには、next()
と同様の追加メソッドreturn()
とthrow()
が2つあります。
next(x)
の動作(最初の呼び出し後)を再確認してみましょう。
yield
演算子で一時停止しています。yield
に値x
を送信します。つまり、x
として評価されます。yield
、return
、またはthrow
に進みます。yield x
は、next()
が{ value: x, done: false }
を返すことになります。return x
は、next()
が{ value: x, done: true }
を返すことになります。throw err
(ジェネレータ内でキャッチされない)は、next()
がerr
をスローすることになります。return()
とthrow()
はnext()
と同様に動作しますが、ステップ2で異なることを行います。
return(x)
は、yield
の位置でreturn x
を実行します。throw(x)
は、yield
の位置でthrow x
を実行します。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 }
finally
句内でyield
を使用すると(その句でreturn
ステートメントを使用することも可能です)、return()
によるジェネレータの終了を防ぐことができます。
function
*
genFunc2
()
{
try
{
console
.
log
(
'Started'
);
yield
;
}
finally
{
yield
'Not done, yet!'
;
}
}
今回は、return()
はジェネレータ関数を終了しません。したがって、それが返すオブジェクトのプロパティdone
はfalse
です。
> 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 }
まだ開始されていない新規ジェネレータからの値の返却は許可されます。
> function* genFunc() {}
> genFunc().return('yes')
{ value: 'yes', done: true }
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
で関数を抜けたことによるものです。
まだ開始されていない新規ジェネレータで例外をスローすることは許可されています。
> function* genFunc() {}
> genFunc().throw(new Error('Problem!'))
Error: Problem!
オブザーバとしてのジェネレータは入力待ちの間一時停止するため、非同期で受信されるデータのオンデマンド処理に最適です。ジェネレータのチェーンを設定するためのパターンは次のとおりです。
target
があります。これは yield
を介してデータを受信し、target.next()
を介してデータを送信します。target
がなく、データを受信するだけです。チェーン全体は、非同期リクエストを行い、next()
を介してジェネレータのチェーンに結果をプッシュする、ジェネレータではない関数によって先頭に付けられています。
例として、非同期で読み取られるファイルを処理するためのジェネレータをチェーンしてみましょう。
次のコードはチェーンを設定します。ジェネレータ splitLines
、numberLines
、printLines
が含まれています。データは、ジェネレータではない関数 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
();
}
});
重要なパターンに注意してください。
readFile
は、ジェネレータオブジェクトメソッド return()
を使用して、送信するチャンクのシーケンスの終了を知らせます。readFile
は、splitLines
が無限ループ内で yield
を介して入力を待っている間にそのシグナルを送信します。return()
はそのループから抜け出します。splitLines
は、シーケンスの終了を処理するために finally
節を使用します。次のジェネレータは 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
);
}
});
このコードの良い点は、すべてが遅延して(オンデマンドで)発生することです。行は、到着するにつれて分割、番号付け、印刷されます。すべてのテキストが到着するまで待たなくても、印刷を開始できます。
yield*
:全体像 大まかな経験則として、yield*
は、あるジェネレータ(呼び出し元)から別のジェネレータ(被呼び出し元)への関数呼び出し(と同等のもの)を実行します。
これまで、yield
の1つの側面しか見ていませんでした。それは、生成された値を被呼び出し元から呼び出し元に伝播することです。入力を受け取るジェネレータに関心を持つようになった今、別の側面が重要になります。yield*
は、呼び出し元が受け取った入力を被呼び出し元にも転送します。ある意味、被呼び出し元がアクティブなジェネレータになり、呼び出し元のジェネレータオブジェクトを介して制御できます。
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()
も同様に転送されます。
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
}
}
}
単純にするために、このコードにはいくつかのものが欠けています。
yield*
のオペランドは、任意の反復可能な値にすることができます。return()
と throw()
は、オプションのイテレータメソッドです。存在する場合にのみ呼び出す必要があります。throw()
が存在しないが return()
が存在する場合は、return()
が呼び出され(例外をスローする前)、calleeObject
にクリーンアップの機会が与えられます。calleeObj
は、プロパティ done
が false
のオブジェクトを返すことで、クローズを拒否できます。その場合、呼び出し元もクローズを拒否する必要があり、yield*
は反復を続行する必要があります。ジェネレータは、データのソースまたはシンクとして使用されるのを見てきました。多くのアプリケーションでは、これらの2つの役割を厳密に分離することが良い習慣です。なぜなら、物事をよりシンプルに保つことができるからです。このセクションでは、ジェネレータの完全なインターフェース(両方の役割を組み合わせたもの)と、両方の役割が必要になる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
;
}
協調的マルチタスクは、出力と入力を両方処理する必要があるジェネレータのアプリケーションです。その仕組みに入る前に、JavaScriptにおける並列処理の現状を確認しましょう。
JavaScriptは単一プロセスで動作します。この制限を解消する方法は2つあります。
2つのユースケースが、協調的マルチタスクから恩恵を受けます。なぜなら、それらは、ほとんどシーケンシャルな制御フローを伴い、時折一時停止するからです。
いくつかの 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行)は、例外をスローする可能性があることを無視しました(例外がジェネレータ関数の本体からエスケープしたとき)。
コルーチンは協調的マルチタスク処理を行うタスクであり、制限がありません。コルーチン内では、任意の関数がコルーチン全体(関数のアクティブ化自体、関数呼び出し元のアクティブ化、呼び出し元の呼び出し元など)を中断できます。
対照的に、ジェネレータはジェネレータ内部から直接しか中断できず、中断されるのは現在の関数アクティブ化のみです。これらの制限により、ジェネレータは時々浅いコルーチン [3]と呼ばれます。
ジェネレータの制限には、主に2つの利点があります。
JavaScriptには、すでに非常に単純なスタイルの協調的マルチタスク処理があります。それは、タスクのキューでの実行をスケジュールするイベントループです。各タスクは関数を呼び出すことで開始され、その関数が完了すると終了します。イベント、setTimeout()
、その他のメカニズムによって、タスクがキューに追加されます。
このマルチタスク処理のスタイルは、実行完了という重要な保証を行います。すべての関数は、完了するまで別のタスクによって中断されないという点に依存できます。関数はトランザクションになり、中間状態にあるデータを見られることなく、完全なアルゴリズムを実行できます。共有データへの同時アクセスはマルチタスクを複雑にし、JavaScriptの同時実行モデルでは許可されていません。そのため、実行完了は良いことです。
しかし、コルーチンは実行完了を妨げます。なぜなら、任意の関数が呼び出し元を中断できるからです。たとえば、次のアルゴリズムは複数のステップで構成されています。
step1(sharedData);
step2(sharedData);
lastStep(sharedData);
step2
がアルゴリズムを中断した場合、アルゴリズムの最後のステップが実行される前に、他のタスクが実行される可能性があります。これらのタスクには、アプリケーションの他の部分を含めることができ、それらはsharedData
を未完成の状態で見ます。ジェネレータは実行完了を維持し、自分自身のみを中断して呼び出し元に返ります。
co
および同様のライブラリは、コルーチンの欠点なしに、そのほとんどの機能を提供します。
yield*
を介して行われた場合のみ中断可能です。これにより、呼び出し元は中断を制御できます。このセクションでは、ジェネレータの使用方法の例をいくつか示します。
反復処理に関する章では、いくつかの反復可能オブジェクトを手動で実装しました。このセクションでは、代わりにジェネレータを使用します。
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
を使用できないため、ジェネレータを介して実装することによる利点はほとんどありません。
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
++
};
}
}
}
map
、filter
配列は、map
およびfilter
メソッドを使用して変換できます。これらのメソッドは、入力を反復可能オブジェクトとし、出力を反復可能オブジェクトとするように一般化できます。
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 ]
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 ]
次の2つの例では、ジェネレータを使用して文字ストリームを処理する方法を示します。
/^[A-Za-z0-9]+$/
に一致する文字列)にグループ化されます。単語以外の文字は無視されますが、単語を区切ります。このステップの入力は文字ストリームであり、出力は単語ストリームです。/^[0-9]+$/
に一致する単語のみを保持し、それらを数値に変換します。巧妙な点は、すべてが遅延(増分的に、オンデマンドで)計算されることです。計算は最初の文字が到着するとすぐに開始されます。たとえば、最初の単語を取得するために、すべての文字が到着するまで待つ必要はありません。
ジェネレータを使用した遅延プルは、次のように機能します。ステップ1〜3を実装する3つのジェネレータは、次のように連結されます。
addNumbers
(
extractNumbers
(
tokenize
(
CHARS
)))
各チェーンメンバーはソースからデータを取得し、アイテムのシーケンスを生成します。処理は、ソースが文字列CHARS
であるtokenize
から開始されます。
次のトリックにより、コードが少しシンプルになります。シーケンスの終了イテレータの結果(プロパティdone
がfalse
である)は、センテンス値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' ]
このステップは比較的シンプルで、数字のみを含む単語を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 ]
文字列は数値に変換されることに注意してください。
/**
* 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
]
ジェネレータのチェーン自体は、出力を作成しません。スプレッド演算子を介して出力を積極的にプルする必要があります。
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'
と' '
が受信されるとすぐに結果を生成することを示しています。
以前のプルベースのアルゴリズムをプッシュベースのアルゴリズムに変換するために必要な作業はほとんどありません。手順は同じです。ただし、プルで終了する代わりに、プッシュで開始します。
前述のように、ジェネレータが 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
このジェネレータが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
このステップは簡単です。
/**
* 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
入力は文字列のシーケンスですが、出力は数値のシーケンスであることに注意してください。
今回は、ストリームの終わりに反応して単一の値をプッシュし、その後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
ジェネレータのチェーンは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
が結果を生成することを示しています。
この例では、ウェブページに表示されるカウンタを作成します。メインスレッドとユーザーインターフェースをブロックしない協調的なマルチタスクバージョンになるまで、初期バージョンを改良します。
これは、カウンタを表示するウェブページの部分です。
<
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
());
ジェネレータ関数(またはメソッド)を呼び出す場合、そのジェネレータオブジェクトにアクセスできません。その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`
}
ライブラリjs-csp
は、JavaScriptに通信シーケンシャルプロセス(CSP)をもたらします。これは、ClojureScriptのcore.asyncやGoのgoroutinesと同様の協調的マルチタスクのスタイルです。js-csp
には2つの抽象化があります。
go()
にジェネレータ関数を渡すことで実装されます。chan()
を呼び出すことで作成されます。例として、関数型リアクティブプログラミングを彷彿とさせる方法で、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
;
}
これは、ECMAScript 6でさまざまなオブジェクトがどのように接続されているかの図です(Allen Wirf-Brockの図に基づいています)。
凡例
x
からy
への白い矢印は、Object.getPrototypeOf(x) === y
を意味します。x
からy
へのinstanceof
矢印は、x instanceof y
を意味します。o instanceof C
はC.prototype.isPrototypeOf(o)
と同等であることを覚えておいてください。x
からy
へのprototype
矢印は、x.prototype === y
を意味します。この図から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!'
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()
などのツールメソッドを含むようになるでしょう(ソース)。
this
の値 ジェネレータ関数は、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
;
アスタリスクのフォーマットに関する妥当で合法的なバリエーションは次のとおりです。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
これらのバリエーションのうち、どの構成でどのバリエーションが意味があり、なぜそうなのかを検討しましょう。
ここでは、generator
(または同様のもの)がキーワードとして使用できないため、アスタリスクのみが使用されています。もしそうであれば、ジェネレータ関数宣言は次のようになります。
generator
foo
(
x
,
y
)
{
···
}
generator
の代わりに、ECMAScript 6はfunction
キーワードにアスタリスクを付けます。したがって、function*
はgenerator
の同義語と見なすことができ、ジェネレータ関数宣言を次のように記述することを示唆しています。
function
*
foo
(
x
,
y
)
{
···
}
匿名ジェネレータ関数式は、次のようにフォーマットされます。
const
foo
=
function
*
(
x
,
y
)
{
···
}
ジェネレータメソッド定義を記述する際には、アスタリスクを次のようにフォーマットすることをお勧めします。
const
obj
=
{
*
generatorMethod
(
x
,
y
)
{
···
}
};
アスタリスクの後にスペースを付けることを支持する3つの理由があります。
第一に、アスタリスクはメソッド名の一部ではありません。一方で、ジェネレータ関数名の一部ではありません。他方で、アスタリスクはジェネレータを定義する場合にのみ言及され、使用する場合には言及されません。
第二に、ジェネレータメソッド定義は次の構文の略記です。(私の主張を明確にするために、関数式にも冗長に名前を付けています)。
const
obj
=
{
generatorMethod
:
function
*
generatorMethod
(
x
,
y
)
{
···
}
};
メソッド定義がfunction
キーワードを省略することについてであれば、アスタリスクの後にスペースを付ける必要があります。
第三に、ジェネレータメソッド定義は、ゲッターとセッター(ECMAScript 5ですでに使用可能)と構文的に似ています。
const
obj
=
{
get
foo
()
{
···
}
set
foo
(
value
)
{
···
}
};
キーワードget
とset
は、通常のメソッド定義の修飾子と見なすことができます。おそらく、アスタリスクもそのような修飾子です。
yield
のフォーマット 以下は、独自の生成値を再帰的に生成するジェネレータ関数の例です。
function
*
foo
(
x
)
{
···
yield
*
foo
(
x
-
1
);
···
}
アスタリスクは異なる種類のyield
演算子をマークしており、そのため上記の書き方が理にかなっています。
Kyle Simpson (@getify)は興味深い提案をしました。Math.max()
などの関数やメソッドについて記述する際に、しばしば括弧を付けることを考えると、ジェネレータ関数やメソッドについて記述する際にアスタリスクを付けるのは理にかなっているのではないでしょうか?例えば、前のセクションのジェネレータ関数を指すために*foo()
と書くべきでしょうか?それに対して反対論を展開しましょう。
反復可能なオブジェクトを返す関数を記述する場合、ジェネレータはいくつかの選択肢のうちの1つに過ぎません。関数名でこの実装の詳細を明らかにしない方が良いと考えます。
さらに、ジェネレータ関数を呼び出す際にアスタリスクを使用することはありませんが、括弧は使用します。
最後に、アスタリスクは有用な情報を提供しません – yield*
は、反復可能なオブジェクトを返す関数でも使用できます。しかし、反復可能なオブジェクトを返す関数やメソッド(ジェネレータを含む)の名前をマークすることは理にかなうかもしれません。例えば、サフィックスIter
を使用するなどです。
function*
キーワードを使用し、generator
を使用しないのはなぜですか? 下位互換性のために、generator
キーワードを使用することはできませんでした。例えば、次のコード(仮説的なES6匿名ジェネレータ式)は、ES5の関数呼び出しとコードブロックの後に続く可能性があります。
generator
(
a
,
b
,
c
)
{
···
}
アスタリスクによる命名スキームはyield*
にもうまく拡張できると考えています。
yield
はキーワードですか? yield
は厳格モードでのみ予約語です。それをES6の非厳格モードに取り込むために、トリックが使用されます。それはコンテキストキーワードになり、ジェネレータ内でのみ使用できるようになります。
この章が、ジェネレータが有用で多用途なツールであることを納得させられたことを願っています。
ジェネレータによって、非同期関数呼び出しを行う際にブロックしながら、協調的にマルチタスク化されたタスクを実装できる点が気に入っています。私の意見では、それは非同期呼び出しの正しいメンタルモデルです。将来的にJavaScriptがこの方向にさらに進むことを願っています。
この章の情報源
[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」