Генераторы

Генераторы это обычно функции, которые возвращают объект класса Generator. Функцию-генератор легко определить по вызову ключевого слова yield. Если в функции существует это ключевое слово и эта функция была вызвана, то код в ней не выполняется. Вместо этого возвращается объект генератора. Код внутри будет выполнен только когда произойдет итерация генератора.

function gen() {
    echo "One";
    yield "Two";
    echo "Three";
    yield "Four";
    echo "Five";
}

$generator = gen();

В данном случае, хотя и была вызвана функция gen(), но код в функции не выполнялся. Вместо этого переменная $generator теперь является генератором.

Сам итератор работает следующим образом. Сначала вызывается метод rewind(), а затем метод valid(), чтобы определить нужна ли вообще итерация. Затем будет вызван метод current() чтобы получить первое значение:

$generator->rewind();
if ($generator->valid()) {
    echo $generator->current();
}

Вызов rewind() произведет вход в нашу функцию-генератор и выполнит весь код, пока не встретит ключевое слово yield. В этот момент он не выполняет выражение yield, только лишь код до него. В нашем случае это означает echo "One".

Так как не был достигнут еще конец генератора, то valid() возвращает TRUE, поэтому вызывается current(), который и выполнит выражение yield ("Two"), возвращая результат в наш echo, и снова код станет на паузу.

Чтобы перейти к следующей итерации вызывается next(), который имеет схожий эффект с rewind(), и возвращается снова в функцию, но на этот раз в место сразу за выражением yield, и продолжает выполнение пока снова не встретит yield. Это означает выполнение echo "Three".

Если же мы дальше продолжим итерацию, то valid() будет вызван и если вернет TRUE, то снова будет вызван current(), возвращая "Four" в echo и снова пауза.

Продолжая дальше к следущей итерации вызывается next(), он выполняет echo "Five", и тут мы встречаем конец функции, что заставляет valid() вернуть FALSE, останавливая итератор:

foreach ($generator as $value) {
    echo $value;
}

Генераторы идельано подходят для операций итерации над данными. Другие примеры использования:

  • чтение больших файлов
  • обработка результатов от базы данных
  • создание HTML таблиц

Закрытие генератора

Генератор считается закрытым когда:

  • он встречает выражение return
  • достигнут конец функции
  • выбрашено и не поймано исключение внутри генератора
  • все ссылки на генератор были удалены

Ключи

Так же можно возвращать ключи из генератора, используя нотацию массивов:

function tagsAndSlugs(array $tags) {
    foreach ($tags as $value) {
        $slug = createSlug($value);
        yield $slug => $value;
    }
}

Ссылки

Генератор так же может возвращать значение по ссылке. Это достигается добавлением & к вызову функции-генератора:

class DataModel
{
    protected $data = [];
    function &getIterator() {
        foreach ($this->data as $key => $value) {
            yield $key => $value;
        }
    }
}

// $dm = instanceof DataModel
foreach ($dm->getIterator() as $key => &$value) {
    $value = strtoupper($value); // $dm->data is updated
}

Переиспользование генератора

Нельзя повторно использовать генератор, так же как обычные итераторы или массивы. Если вызвать rewind() у генератора после первого вызова yield, то будет выброшено исключение. Это означает, что нельзя произвести итерацию над одним и тем же генератором дважды.

Так же нельзя клонировать генераторы. Это вызовет фатальную ошибку: Fatal error: Trying to clone an uncloneable object of class Generator..

Если нужно повторно использовать генератор, то следует заново вызвать функцию, создавая новый объект генератора.

Корутины

Главное, что корутины добавляют к вышеописанной функциональности — это возможность отправлять значения генератору. Это делает из привычного монолога генератора полноценный диалог, где вызывающая сторона может также что-то сообщать генератору.

Значения передаются корутине вызовом метода send() вместо next(). Примером того, как это работает, послужит вот эта корутина:

function logger($fileName) {
    $fileHandle = fopen($fileName, 'a');
    while (true) {
        fwrite($fileHandle, yield . "\n");
    }
}

$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar');

Здесь yield не как statement (как например return или echo), а как выражение, то есть он возвращает какое-то значение. Он возвратит то, что было послано через send(). В данном примере yield сначала возвратит "Foo", а потом "Bar".