PHP5.5で追加されたジェネレータについて調べた

前にまとめたPHP5系で追加された機能を振り返る記事の続き。今回は、ほぼ知らなかったジェネレータについて調べた。サンプルコードを多めに載せ、まとめていく。

ジェネレータとは

ジェネレータとは、イテレータのような反復処理をシンプルに実装できる構文である。

例えば、range()関数をジェネレータで実装し直すと以下のようになる。

<?php

function xRange($start, $limit, $step = 1)
{
    for ($i = $start; $i <= $limit; $i += $step) {
        yield $i;
    }
}

foreach (xRange(1, 10, 2) as $number) {
    echo "{$number}\n"; // 1 3 5 7 9
}

通常のrange()関数の場合、指定された範囲の値を含む配列を返すため、配列のサイズ分メモリを使用してしまうが、xRange()関数のようなジェネレータ関数を実装することでメモリの節約をすることができる。

ジェネレータの構文について

yieldキーワード

ジェネレータ関数で最も重要となるのが、このyieldキーワードである。returnのように値を返して関数の処理が終了するのではなく、yieldはジェネレータ関数の呼び出し元に値を返し、そこでジェネレータ関数の実行を一時停止する。上記のxRange()関数のように、ジェネレータ関数内でループ中であっても、処理を一時停止し、再びループを再開することができる。

また、yieldは以下のようなキーと値のペアを返すこともできる。

<?php

function getRandomValue()
{
    $counter = 0;
    while (true) {
        yield ++$counter => rand(0, 10);
    }
}

foreach (getRandomValue() as $key => $value) {
    echo "{$key}: {$value}\n";
    if ($value == 0) {
        break;
    }
}

Generatorクラスについて

ジェネレータ関数が最初に呼び出されたとき、Generatorクラスのオブジェクトを返す。このオブジェクトはIteratorインターフェイスを実装していて、このオブジェクトのメソッドを使用することで、ジェネレータを操作することができる。

例えば、上のgetRandomValue()関数の呼び出しは以下のように書き換えることもできる。

<?php

$rnd = getRandomValue();
while (true) {
    $key = $rnd->key(); // yieldしたキーを取得
    $value = $rnd->current(); // yieldした値を取得

    echo "{$key}: {$value}\n";

    if ($value == 0) {
        break;
    }

    $rnd->next(); // ジェネレータを続行する
}

Generatorクラスは他にも便利なメソッドがあるが、それらについては後述する。

インタラクションについて

Generatorクラスにはsend()メソッドが備わっている。このメソッドは、指定した値をyieldの結果としてジェネレータに送ることができる。これを用いることで、双方向で値を受け渡しすることができ、例えば、以下のような数あてゲームのようなことができるようになる。

<?php
$ng = (function ($ans)
{
    $input = yield;

    while ($input != $ans) {
        if ($input < $ans) {
            $input = yield "\$ans > {$input}";
        } else {
            $input = yield "\$ans < {$input}";
        }
    }
})(rand(0, 100));

echo "Start\n";
do {
    fscanf(STDIN,'%d', $ans);
    $hint = $ng->send($ans);
    echo ($hint ?: 'Correct!!') . PHP_EOL;
} while ($hint);

PHP7.0から追加された機能

yield fromキーワード

このキーワードを用いることで、別のジェネレータやTraversableオブジェクトや配列から順に値を取り出し、それぞれの値をyieldすることができる。

<?php

function f1()
{
    yield 1;
    yield from [2, 3];
    yield from f2();
    yield 6;
}

function f2()
{
    yield 4;
    yield 5;
}

foreach(f1() as $value){
    echo "{$value}\n"; // 1 2 3 4 5 6
}

returnキーワード

PHP5まではジェネレータ関数内でreturnできなかった。returnされたタイミングでジェネレータ関数は終了し、このreturnの戻り値は通常のreturnのように値を受け取るのではなく、GeneratorクラスのgetReturn()メソッドで取得することができる。

<?php

function getNumbers()
{
    for ($i = 0; $i < 10; ++$i) {
        yield $i;
    }

    return "Finish!!";
}

foreach ($gen = getNumbers() as $value) {
    echo "{$value}\n";
}
echo $gen->getReturn() . PHP_EOL;

所感

ジェネレータをうまく活用しているのをこれまでほとんど見たことがないので、どのように活用すれば良いかパッと思いつかないが、PHPの機能としてはとても面白いと思った。

参考