ジェネレータの構文
ジェネレータ関数の見た目はふつうの関数とほぼ同じです。違うのは、値を返すのではなく、 必要なだけ値を yield することです。 yield が含まれていれば、どんな関数でもジェネレータ関数です。
ジェネレータ関数が呼ばれると、反復処理が可能なオブジェクトを返します。 このオブジェクトを (foreach ループなどで) 反復させると、 値が必要になるたびに PHP がオブジェクトの反復メソッドを呼びます。 そして、ジェネレータが値を yield した時点の状態を保存しておき、 次に値が必要になったときにはそこから再開できるようにします。
yield できる値がなくなると、ジェネレータは単純に呼び出し元に制御を戻します。 呼び出し元のコードでは、配列の要素をすべて処理し終えた後のように、そのまま処理が続きます。
注意:
ジェネレータは値を返すことができます。返した値は Generator::getReturn() で取得することが出来ます。
yield キーワード
ジェネレータ関数の肝となるのが yield キーワードです。 最もシンプルな書きかたをすると、yield 文の見た目は return 文とほぼ同じになります。 ただ、return の場合はそこで関数の実行を終了して値を返すのに対して、 yield の場合はジェネレータを呼び出しているループに値を戻して ジェネレータ関数の実行を一時停止します。
例1 値を yield する単純な例
<?php
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
// yield を繰り返す間、$i の値が維持されることに注目しましょう
yield $i;
}
}
$generator = gen_one_to_three();
foreach ($generator as $value) {
echo "$value\n";
}
?>
上の例の出力は以下となります。
1 2 3
注意:
内部的には整数の連番のキーが yield する値とペアになり、 配列と同じようになります。
値とキーの yield
PHP は、数値添字の配列だけでなく連想配列にも対応しています。ジェネレータも例外ではありません。 先ほどの例のように単なる値を yield するだけでなく、 値と同時にキーも yield することができます。
キーと値のペアを yield する構文は連想配列の定義とよく似ており、次のようになります。
例2 キー/値 のペアの yield
<?php
/*
* 入力は各フィールドをセミコロンで区切ったものです
* 最初のフィールドが ID となり、これをキーとして使います
*/
$input = <<<'EOF'
1;PHP;$が大好き
2;Python;インデントが大好き
3;Ruby;ブロックが大好き
EOF;
function input_parser($input) {
foreach (explode("\n", $input) as $line) {
$fields = explode(';', $line);
$id = array_shift($fields);
yield $id => $fields;
}
}
foreach (input_parser($input) as $id => $fields) {
echo "$id:\n";
echo " $fields[0]\n";
echo " $fields[1]\n";
}
?>
上の例の出力は以下となります。
1: PHP $が大好き 2: Python インデントが大好き 3: Ruby ブロックが大好き
null 値の yield
何も引数を渡さずに yield を呼ぶと、null
値を yield します。キーは自動的に割り振られます。
例3 null
の yield
<?php
function gen_three_nulls() {
foreach (range(1, 3) as $i) {
yield;
}
}
var_dump(iterator_to_array(gen_three_nulls()));
?>
上の例の出力は以下となります。
array(3) { [0]=> NULL [1]=> NULL [2]=> NULL }
参照による yield
ジェネレータ関数は、値を参照として yield することもできます。 関数の結果を参照で返す ときと同じように、関数名の前にアンパサンドを付けます。
例4 参照による値の yield
<?php
function &gen_reference() {
$value = 3;
while ($value > 0) {
yield $value;
}
}
/*
* $number をループ内で変更していることに注目しましょう。
* このジェネレータは参照を yield するので、
* gen_reference() 内の $value が変わります。
*/
foreach (gen_reference() as &$number) {
echo (--$number).'... ';
}
?>
上の例の出力は以下となります。
2... 1... 0...
yield from によるジェネレータの委譲
ジェネレーターを委譲することで、 別のジェネレータや Traversable オブジェクトあるいは配列から、 yield from キーワードを使って値を yield できます。 外側のジェネレータは、内側のジェネレータ (あるいはオブジェクトや配列) から受け取れるすべての値を yield し、 何も取得できなくなったら外側のジェネレータの処理を続行します。
ジェネレータに対して yield from を使った場合は、 yield from 式は内側のジェネレータが返す任意の値を返します。
iterator_to_array() を用いた、配列への格納
yield from は配列のキーをリセットしません。 Traversable オブジェクトや array が返すキーを、そのまま利用します。つまり、別々の yield や yield from から取得した異なる値のキーが、重複することもありえます。 これを配列に格納すると、後からきた値がそれまでの値を上書きします。
iterator_to_array() を使う場合に問題になることがよくあります。
この関数はデフォルトで数値添字配列を返すので、予期せぬ結果を引き起こす可能性があります。
iterator_to_array() には二番目のパラメータ
preserve_keys
があり、これを false
にすれば、Generator が返すキーを無視してすべての値を取得できます。
例5 yield from と iterator_to_array()
<?php
function inner() {
yield 1; // キー 0
yield 2; // キー 1
yield 3; // キー 2
}
function gen() {
yield 0; // キー 0
yield from inner(); // キー 0〜2
yield 4; // キー 1
}
// 二番目のパラメータに false を指定すると、結果は array [0, 1, 2, 3, 4] となります
var_dump(iterator_to_array(gen()));
?>
上の例の出力は以下となります。
array(3) { [0]=> int(1) [1]=> int(4) [2]=> int(3) }
例6 yield from の基本的な使いかた
<?php
function count_to_ten() {
yield 1;
yield 2;
yield from [3, 4];
yield from new ArrayIterator([5, 6]);
yield from seven_eight();
yield 9;
yield 10;
}
function seven_eight() {
yield 7;
yield from eight();
}
function eight() {
yield 8;
}
foreach (count_to_ten() as $num) {
echo "$num ";
}
?>
上の例の出力は以下となります。
1 2 3 4 5 6 7 8 9 10
例7 yield from の返す値
<?php
function count_to_ten() {
yield 1;
yield 2;
yield from [3, 4];
yield from new ArrayIterator([5, 6]);
yield from seven_eight();
return yield from nine_ten();
}
function seven_eight() {
yield 7;
yield from eight();
}
function eight() {
yield 8;
}
function nine_ten() {
yield 9;
return 10;
}
$gen = count_to_ten();
foreach ($gen as $num) {
echo "$num ";
}
echo $gen->getReturn();
?>
上の例の出力は以下となります。
1 2 3 4 5 6 7 8 9 10
User Contributed Notes 9 notes
For example yield keyword with Fibonacci:
function getFibonacci()
{
$i = 0;
$k = 1; //first fibonacci value
yield $k;
while(true)
{
$k = $i + $k;
$i = $k - $i;
yield $k;
}
}
$y = 0;
foreach(getFibonacci() as $fibonacci)
{
echo $fibonacci . "\n";
$y++;
if($y > 30)
{
break; // infinite loop prevent
}
}
[This comment replaces my previous comment]
You can use generators to do lazy loading of lists. You only compute the items that are actually used. However, when you want to load more items, how to cache the ones already loaded?
Here is how to do cached lazy loading with a generator:
<?php
class CachedGenerator {
protected $cache = [];
protected $generator = null;
public function __construct($generator) {
$this->generator = $generator;
}
public function generator() {
foreach($this->cache as $item) yield $item;
while( $this->generator->valid() ) {
$this->cache[] = $current = $this->generator->current();
$this->generator->next();
yield $current;
}
}
}
class Foobar {
protected $loader = null;
protected function loadItems() {
foreach(range(0,10) as $i) {
usleep(200000);
yield $i;
}
}
public function getItems() {
$this->loader = $this->loader ?: new CachedGenerator($this->loadItems());
return $this->loader->generator();
}
}
$f = new Foobar;
# First
print "First\n";
foreach($f->getItems() as $i) {
print $i . "\n";
if( $i == 5 ) {
break;
}
}
# Second (items 1-5 are cached, 6-10 are loaded)
print "Second\n";
foreach($f->getItems() as $i) {
print $i . "\n";
}
# Third (all items are cached and returned instantly)
print "Third\n";
foreach($f->getItems() as $i) {
print $i . "\n";
}
?>
If for some strange reason you need a generator that doesn't yield anything, an empty function doesn't work; the function needs a yield statement to be recognised as a generator.
<?php
function gndn()
{
}
foreach(gndn() as $it)
{
echo 'FNORD';
}
?>
But it's enough to have the yield syntactically present even if it's not reachable:
<?php
function gndn()
{
if(false) { yield; }
}
foreach(gndn() as $it)
{
echo 'FNORD';
}
?>
Do not call generator functions directly, that won't work.
<?php
function my_transform($value) {
var_dump($value);
return $value * 2;
}
function my_function(array $values) {
foreach ($values as $value) {
yield my_transform($value);
}
}
$data = [1, 5, 10];
// my_transform() won't be called inside my_function()
my_function($data);
# my_transform() will be called.
foreach (my_function($data) as $val) {
// ...
}
?>
That is a simple fibonacci generator.
<?php
function fibonacci($item) {
$a = 0;
$b = 1;
for ($i = 0; $i < $item; $i++) {
yield $a;
$a = $b - $a;
$b = $a + $b;
}
}
# give me the first ten fibonacci numbers
$fibo = fibonacci(10);
foreach ($fibo as $value) {
echo "$value\n";
}
?>
<?php
//Example of class implementing IteratorAggregate using generator
class ValueCollection implements IteratorAggregate
{
private $items = array();
public function addValue($item)
{
$this->items[] = $item;
return $this;
}
public function getIterator()
{
foreach ($this->items as $item) {
yield $item;
}
}
}
//Initializes a collection
$collection = new ValueCollection();
$collection
->addValue('A string')
->addValue(new stdClass())
->addValue(NULL);
foreach ($collection as $item) {
var_dump($item);
}
This is little example of using generators with recursion. Used version of php is 5.5.5
[php]
<?php
define ("DS", DIRECTORY_SEPARATOR);
define ("ZERO_DEPTH", 0);
define ("DEPTHLESS", -1);
define ("OPEN_SUCCESS", True);
define ("END_OF_LIST", False);
define ("CURRENT_DIR", ".");
define ("PARENT_DIR", "..");
function DirTreeTraversal($DirName, $MaxDepth = DEPTHLESS, $CurrDepth = ZERO_DEPTH)
{
if (($MaxDepth === DEPTHLESS) || ($CurrDepth < $MaxDepth)) {
$DirHandle = opendir($DirName);
if ($DirHandle !== OPEN_SUCCESS) {
try{
while (($FileName = readdir($DirHandle)) !== END_OF_LIST) { //read all file in directory
if (($FileName != CURRENT_DIR) && ($FileName != PARENT_DIR)) {
$FullName = $DirName.$FileName;
yield $FullName;
if(is_dir($FullName)) { //include sub files and directories
$SubTrav = DirTreeTraversal($FullName.DS, $MaxDepth, ($CurrDepth + 1));
foreach($SubTrav as $SubItem) yield $SubItem;
}
}
}
} finally {
closedir($DirHandle);
}
}
}
}
$PathTrav = DirTreeTraversal("C:".DS, 2);
print "<pre>";
foreach($PathTrav as $FileName) printf("%s\n", $FileName);
print "</pre>";
[/php]
If you mix yielding values with keys and yielding values without keys, the result is the same as adding values to an array with or without keys.
<?php
function gen() {
yield 'a';
yield 4 => 'b';
yield 'c';
}
$t = iterator_to_array(gen());
var_dump($t);
?>
The result is an array [0 => 'a', 4 => 'b', 5 => 'c'], just as if you had written
<?php
$t = [];
$t[] = 'a';
$t[4] = 'b';
$t[] = 'c';
var_dump($t);
?>
With the key given to 'c' being incremented from the previous numeric index.
Module list of a number from 1 to 100.
<?php
function list_of_modulo(){
for($i = 1; $i <= 100; $i++){
if(($i % 2) == 0){
yield $i;
}
}
}
$modulos = list_of_modulo();
foreach($modulos as $value){
echo "$value\n";
}
?>