ジェネレータの構文

ジェネレータ関数の見た目はふつうの関数とほぼ同じです。違うのは、値を返すのではなく、 必要なだけ値を 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 が返すキーを、そのまま利用します。つまり、別々の yieldyield from から取得した異なる値のキーが、重複することもありえます。 これを配列に格納すると、後からきた値がそれまでの値を上書きします。

iterator_to_array を使う場合に問題になることがよくあります。 この関数はデフォルトで数値添字配列を返すので、予期せぬ結果を引き起こす可能性があります。 iterator_to_array には二番目のパラメータ preserve_keys があり、これを false にすれば、Generator が返すキーを無視してすべての値を取得できます。

例5 yield fromiterator_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