Koaをやるにあたって、やっぱりジェネレータを知らなければ話にならないようです。
「ジェネレータを使ってコールバックを使わずに非同期処理できる!」と聞いても、「なんでやねん。。。」としかならなかったので、ここは一つ、腰を据えてジェネレータについて勉強しておきたいところです
ジェネレータとは
ジェネレータの意味
ジェネレータは「生成器」なんて訳されていますが、プログラムの世界では次々と値を生成する仕組みのことのようです。
もう少し狭義の世界で考えると、イテレータを実現する仕組みの一種となっていたりもします。
PHPにも5.5以降はジェネレータの仕組みが有りますが、そこでは「イテレータを簡単に実装できる仕組み」となっています。
Javascript の解説ページなどを見ると、やはりイテレータとセットになっている所も多いようです
ですが、どちらかと言うとジェネレータはあくまで値を次々生成する仕組みであり、機能の一部としてイテレーションが入っているように思います
ジェネレータの実装
ジェネレータを早速実装してみましょう
'use strict'
// ジェネレータの定義
function* MakeNumber() {
yield 1;
yield 2;
yield 3;
}
let gen = MakeNumber();// ジェネレータの初期化
console.log(gen.next());// { value: 1, done: false }
console.log(gen.next());// { value: 2, done: false }
console.log(gen.next());// { value: 3, done: false }
console.log(gen.next());// { value: undefined, done: true }
ジェネレータの定義方法は、function
の代わりにfunction*
で定義し、定義の中で幾つかのyield
が含まれています。
次にジェネレータを初期化し変数gen
をジェネレータにします。
ジェネレータgen
はnext
メソッドを持っています。
next
の役割は以下のとおりです
- ジェネレータ関数の定義の現在位置から
yield
のある位置まで処理をすすめる -
yield
キーワードの後に書かれた式を返却値とし、処理位置をそこで止める - 正確な返却値は
{value: <yieldキーワードの後に書かれた式>, done: false}
となる - 後ろにyieldがない状態でnextを呼ぶと、{value: undefined, done: true}を返す
つまり、next
メソッドを呼ぶたびに、各yield
まで処理が進み、一旦値を返すという処理が繰り返されます。
最後のyield
の処理が終わると、もう値を返せないので以降のnext
はvalue
がなしで(value: undefined
)、ジェネレータの処理が完了しているサイン(done: true
)を返します。
無限生成
ループとyield
を組み合わせることで、無限に生成し続けるジェネレータを作ることができます
'use strict'
// 階乗計算
function* Factorial() {
let i = 1;
let num = 1;
while (true) {
num *= i++
yield (i - 1) + '! = ' + num;
}
}
let gen = Factorial();// ジェネレータの初期化
console.log(gen.next());// { value: '1! = 1', done: false }
console.log(gen.next());// { value: '2! = 2', done: false }
console.log(gen.next());// { value: '3! = 6', done: false }
console.log(gen.next());// { value: '4! = 24', done: false }
これはいくらnext
を呼びつづけても、延々と値を生成し続けますので、done
がtrue
になることはないでしょう
イテレータとジェネレータ
イテレータの反復処理の対象としてジェネレータを選択することができます。
'use strict'
// 階乗計算
function* Factorial() {
let i = 1;
let num = 1;
while (i < 7) {
num *= i++
yield {number: (i - 1), fact: num}
}
}
let gen = Factorial();// ジェネレータの初期化
// 反復処理の対象としてジェネレータを選択する
for (let obj of gen) {
console.log(obj.number + '! = ' + obj.fact);
if (obj.number >= 10) {
break;
}
}
結果はこんな感じです
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
まず、反復対象としてジェネレータを選んだ場合、各要素obj
にはは'next'メソッドで返却されるオブジェクトのvalue
が代入されることがわかります。
続いてfor文の中ではobj.number
が10になるまではループを続けられるようになっていますが、実際にはobj.number
は6までで打ち止めになっています。
これはnext
メソッドで返却されるオブジェクトのdone
がtrue
になっていると、反復処理が終了することを意味しています。
ジェネレータの中のジェネレータ
yield*
を使うことで、ジェネレータの中で別のジェネレータに処理を移譲することもできます。
'use strict'
// 入れ子のジェネレータ
function* Subgen() {
yield 3;
yield 4;
}
function* Maingen() {
yield 1;
yield 2;
yield* Subgen();
yield 5;
}
let gen = Maingen();// ジェネレータの初期化
for (let num of gen) {
console.log(num);
}
結果は以下のとおりです
1
2
3
4
5
このyield*
は、ジェネレータにかぎらず反復処理の対象にできるものであれば何でも指定できるので、例えばyield* Subgen()
の部分をyield* [3, 4]
に書き換えても、全く同じ動作をします。
yieldの「返り値」
yield
はこれまで見てきたように、ジェネレータの中ではその処理を中断し、右にある式の値を返します。
では、再度next
を呼び出して処理を行うとき、どこから始まるのでしょうか?
答えは簡単で、yield
から始まります。
しかも、このとき、yield
にはnext
メソッドに与えられた引数が展開されます。
次のコードを見てください
'use strict'
function* Maingen() {
while (true) {
let x = yield 1;
console.log(x);
}
}
let gen = Maingen();
gen.next();// <何も出力されない>
gen.next();// undefined
gen.next(1);// 1
gen.next();// undefined
gen.next('A');// A
一回目のnext
では、そもそもconsole.log
を通過する前に止まってしまうので、何も出力されません。
続いて、二回目のnext
では引数に何も指定していないため、ジェネレータ内部のx
には何も代入されません。したがってundefinedが出力されます。
三回目のnext
には1が引数として与えられています。すると、yield
に1が入り、x
に1が代入されます。したがって、console.log
によって1が出力されるというわけです。
その後も内容は同じです。四回目は何も指定していないので、undefinedになり、五回目は'A'を指定したので、Aが出力されます。
まとめ
ES2015の目玉機能であるところのジェネレータについて見てみました。
このジェネレータが非同期処理とどう絡むかについて、次回見てみたいと思います。