ジェネレータ関数の見た目はふつうの関数とほぼ同じです。違うのは、値を返すのではなく、 必要なだけ値を yield することです。
ジェネレータ関数が呼ばれると、反復処理が可能なオブジェクトを返します。 このオブジェクトを (foreach ループなどで) 反復させると、 値が必要になるたびに PHP がジェネレータ関数を呼びます。 そして、ジェネレータが値を yield した時点の状態を保存しておき、 次に値が必要になったときにはそこから再開できるようにします。
yield できる値がなくなると、ジェネレータ関数は何もせず単純に終了します。 呼び出し元のコードでは、配列の要素をすべて処理し終えた後のように、そのまま処理が続きます。
注意:
ジェネレータは値を返すことができません。値を返そうとすると、コンパイルエラーになります。 ジェネレータの中で空の return 文を書いても文法上は問題ありませんが、 そこでジェネレータは終了します。
ジェネレータ関数の肝となるのが 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 を式のコンテキスト (代入文の右辺など) で使うときは、yield 文を括弧で囲む必要があります。 たとえば、PHP 5 では次のようになります。
$data = (yield $value);
次のように書くと、PHP 5 ではパースエラーになります。
$data = yield $value;
PHP 7 では、この制限はありません。
この構文は、 Generator::send() メソッドと組み合わせて使えます。
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 ブロックが大好き
先ほどの例のように値だけを yield するときと同様に、 キー/値 のペアを式のコンテキストで yield するときにも yield 文を括弧で囲む必要があります。
$data = (yield $key => $value);
何も引数を渡さずに 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 することもできます。 関数の結果を参照で返す ときと同じように、関数名の前にアンパサンドを付けます。
例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...
PHP 7 では、ジェネレータの委譲ができるようになりました。 別のジェネレータや Traversable オブジェクトあるいは配列から、 array by using the yield from キーワードを使って値を yield できます。 外側のジェネレータは、内側のジェネレータ (あるいはオブジェクトや配列) から受け取れるすべての値を yield し、 何も取得できなくなったら外側のジェネレータの処理を続行します。
ジェネレータに対して yield from を使った場合は、 yield from 式は内側のジェネレータが返す任意の値を返します。
yield from は配列のキーをリセットしません。 Traversable オブジェクトや array が返すキーを、そのまま利用します。つまり、別々の yield や yield from から取得した異なる値のキーが、重複することもありえます。 これを配列に格納すると、後からきた値がそれまでの値を上書きします。
iterator_to_array() を使う場合に問題になることがよくあります。
この関数はデフォルトで数値添字配列を返すので、予期せぬ結果を引き起こす可能性があります。
iterator_to_array() には二番目のパラメータ
use_keys
があり、これを FALSE
にすれば、Generator が返すキーを無視してすべての値を取得できます。
例5 yield from と iterator_to_array()
<?php
function from() {
yield 1; // キー 0
yield 2; // キー 1
yield 3; // キー 2
}
function gen() {
yield 0; // キー 0
yield from from(); // キー 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