ジェネレータとは、yieldを含む関数です。呼び出しても本体は実行されず、foreachでループできるGeneratorオブジェクトが返ります。yield $valueはそれぞれ1つの値をループに渡し、ループが次を求めるまで関数を一時停止します。そのため値は、先に配列に集められるのではなく、1つずつ作られます。
出力はジェネレータとループの間を行き来します。各値は、ループが求めたときにだけ作られます。
ジェネレータはメモリを節約する
range(1, 1000000)は、ループが始まる前に100万個の整数の配列を作ります。同じ数をyieldするジェネレータは、一度に1つしか持ちません。実行して、それぞれに必要なメモリを比べてください。
どちらのループも同じ合計500000500000を出力しますが、配列版はextra memory 16.0 MB、ジェネレータはextra memory 0.0 MBと報告します。データが増えるほど差は広がります。大きなファイル、データベースの結果、APIのページにジェネレータが役立つのも同じ考え方からで、1件処理して捨て、次の1件を取るのです。
キーと値をyieldする
yield $key => $valueは、foreach ($gen as $key => $value)が受け取るキーを設定します。キーがなければ、ジェネレータはリストのように値に0から番号を振ります。よくある使い方は、行番号付きでファイルを1行ずつ読むことです。
メモリにあるのは一度に1行だけなので、同じ関数で2 GBのログファイルも扱えます。ここで使っているファイル関数はfopen()とfgets()で扱っています。
無限の数列と遅延パイプライン
ジェネレータは、使う側が求めるところまでしか実行されないので、永遠にループしてもかまいません。breakで止めるか、最初のいくつかの値を取る別のジェネレータで包みます。小さなジェネレータをつなげると、すべての段階が遅延評価されるパイプラインになります。
fibonacci()は自分では決して終わりませんが、プログラムは終了します。take()が求めた数のあとで要求をやめ、その先は何も計算されないからです。
yield from:別のジェネレータに任せる
yield fromは、配列、イテレータ、別のジェネレータのすべての値を受け渡します。ジェネレータを小さなものに分けたり、ネストした構造を再帰的にたどったりする方法です。
yield fromは内側のキーを保つので、どちらの配列もキー0と1をyieldします。そのためiterator_to_array()はaとbを上書きしてしまいます。値だけが重要なときは、2つ目の引数にfalseを渡しましょう。
send()とgetReturn()
ジェネレータは値を受け取ることもできます。$x = yield;で一時停止して待ち、$gen->send($value)はそのyieldの結果を$valueとしてジェネレータを再開します。またジェネレータは最終的な値をreturnでき、終わったあとにgetReturn()で読めます。
ほとんどのコードではsend()は必要なく、foreachでの遅延読み込みが日常的な使い方です。send()はコルーチンのライブラリを支える基本の部品です。
ジェネレータは1回しかループできない
ジェネレータは前に進むだけで、巻き戻せません。foreachで使い切ったあと、同じオブジェクトをもう一度ループすると例外が投げられます。
直すには、ループごとにジェネレータ関数を呼び直す(foreach (colors() as $c))か、本当に2回必要ならiterator_to_array(colors(), false)で一度だけ値を集めます。それを頻繁にしているなら、そのデータには普通の配列が正しい選択でした。
よくある質問
PHPのyieldは何をしますか?
yield $valueは、関数をループしているコードに値を1つ渡し、その場で関数を一時停止します。ループの次の繰り返しで、yieldのあとから再開します。yieldを含む関数は、呼び出しても実行されず、代わりにGeneratorオブジェクトを返します。
配列ではなくジェネレータを使うのはなぜですか?
配列はすべての要素を一度にメモリに持ちますが、ジェネレータは要素を1つずつ作ります。range(1, 1000000)をループすると100万個の整数がすべて確保されますが、それらをyieldするジェネレータならメモリは一定のままです。ジェネレータなら無限の数列も扱え、ループがbreakすれば処理も早めに止まります。
PHPのyield fromとは何ですか?
yield from $sourceは、別のジェネレータ、配列、Traversableのすべての値を、今のジェネレータ自身がyieldしたかのように受け渡します。式としては、内側のジェネレータの戻り値になります。
ジェネレータを2回ループできますか?
できません。ジェネレータは前にしか進みません。foreachが終わったあとにもう一度ループすると、Exception: Cannot traverse an already closed generatorが投げられます。新しいジェネレータを得るにはジェネレータ関数をもう一度呼ぶか、2回必要ならiterator_to_array()で値を保存します。
ジェネレータを配列に変換するには?
iterator_to_array($gen)は、ジェネレータのキーを保ったまますべての値を集めます。yield fromのあとなどでキーが重複すると、あとの値が前の値を上書きするので、普通のリストが欲しいときは2つ目の引数にfalseを渡します:iterator_to_array($gen, false)。