Как работает yield

Yield это ключевое слово, которое используется примерно как return — отличие в том, что функция вернёт генератор.

<?
# нумерует входящий поток данных (возвращает пары номер и значение),
function enumerate($arr)
{
    $i = 0;
 
    foreach ($arr as $value) {
        yield [$i++, $value];
    }
}
 
# фильтрует значения через функцию,
function ifilter(callable $predicate = null, $arr)
{
    if ($predicate === null) {
        $predicate = 'boolval';
    }
 
    foreach ($arr as $value) {
        if ($predicate($value)) {
            yield $value;
        }
    }
}
 
# отрезает заданную часть от входных данных
function islice($arr, $start, $stop)
{
    if (is_array($arr)) {
        reset($arr);
 
        for ($i = $start; $i > 0 && each($arr); $i--);
 
        for ($i = $stop - $start + 1; $i > 0 && list(,$value) = each($arr); $i--) {
            yield $value;
        }
    } else {
        for ($i = $start; $i > 0 && $arr->valid(); $i--) {
            $arr->next();
        }
 
        for ($i = $stop - $start; $i > 0 && $arr->valid(); $i--) {
            yield $arr->current();
            $arr->next();
        }
    }
}
 
$array = [10, 20, 30, 33, 40, 50, 60];
 
$gen = islice(
    enumerate(
        ifilter(
            function($x) { return ($x % 10) === 0; },
            $array
        )),
        1, 4
    );
 
foreach ($gen as $value) {
    print_r($value);
}

PHP поддерживает два способа их создания (как и Пайтон), но мне милее оператор yield. Работает он примерно как return, но при повторном входе в функцию её выполнение происходит не сначала, а с того места, откуда управление было возвращено при помощи yield.

Функция или метод, в котором встречается ключевое слово yield автоматически становится генератором:

function getLinesFromFile($fileName) {
    // на каждой итерации выполняем всё до первого yield
    if (!$fileHandle = fopen($fileName, 'r')) {
        return;
    }
 
    // 
    while (false !== $line = fgets($fileHandle)) {
        // то есть до сюда
        yield $line;
    }
 
    // а вот эта часть выполнится только когда не вызовется yield
    fclose($fileHandle);
}
 
// выполнения функции не происходит потому как внутри есть yield
$lines = getLinesFromFile($fileName);
 
foreach ($lines as $line) {
    // работаем с $line
}
<?php
function xrange($start, $limit, $step = 1) {
    if ($start < $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be +ve');
        }
 
        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be -ve');
        }
 
        for ($i = $start; $i >= $limit; $i += $step) {
            yield $i;
        }
    }
}
 
echo 'Single digit odd numbers from range():  ';
foreach (range(1, 9, 2) as $number) {
    echo "$number ";
}
echo "\n";
 
echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
    echo "$number ";
}
?>

Ответ

Single digit odd numbers from range():  1 3 5 7 9 
Single digit odd numbers from xrange(): 1 3 5 7 9 

Перебор данных файла

function getLines($file) {
    f = fopen($file, 'r');
    try {
        while ($line = fgets($f)) {
            yield $line;
        }
    } finally {
        fclose($f);
    }
}
 
foreach(getLines($file_name) as $row){
 
    if ($n > 5) break; // (вызовется finally)
 
    echo $line;
 
}
 
// (вызовется finally)

Проверяем скорость работы

<?php
 
error_reporting(E_ALL);
 
function xrange($start, $end, $step = 1) {
    for ($i = $start; $i < $end; $i += $step) {
        yield $i;
    }
}
 
class RangeIterator implements Iterator {
    protected $start;
    protected $end;
    protected $step;
 
    protected $key;
    protected $value;
 
    public function __construct($start, $end, $step = 1) {
        $this->start = $start;
        $this->end   = $end;
        $this->step  = $step;
    }
 
    public function rewind() {
        $this->key   = 0;
        $this->value = $this->start;
    }
 
    public function valid() {
        return $this->value < $this->end;
    }
 
    public function next() {
        $this->value += $this->step;
        $this->key   += 1;
    }
 
    public function current() {
        return $this->value;
    }
 
    public function key() {
        return $this->key;
    }
}
 
function urange($start, $end, $step = 1) {
    $result = [];
    for ($i = $start; $i < $end; $i += $step) {
        $result[] = $i;
    }
    return $result;
}
 
function testTraversable($name, callable $traversableFactory) {
    $startTime = microtime(true);
    foreach ($traversableFactory() as $value) {
        // noop
    }
    echo $name, ' took ', microtime(true) - $startTime, ' seconds.', "\n";
}
 
function testVariants($count) {
    testTraversable(
        "xrange        ($count)",
        function() use($count) { return xrange(0, $count); }
    );
    testTraversable(
        "RangeIterator ($count)",
        function() use($count) { return new RangeIterator(0, $count); }
    );
    testTraversable(
        "urange        ($count)",
        function() use($count) { return urange(0, $count); }
    );
    testTraversable(
        "range         ($count)",
        function() use($count) { return range(0, $count); }
    );
}
 
testVariants(1000000);
testVariants(10000);
testVariants(100);

Генераторы позволяют отправлять себе данные, используя метод send(). В некоторых случаях это может быть очень удобно. Например, когда надо сделать какой-то лог-файл. Вместо того, чтобы писать целый класс для него, можно просто воспользоваться генераторами:

function createLog($file) {
    $f = fopen($file, 'a');
    while (true) {          # да, опять бесконечный цикл;
        $line = yield;      # бесконечно "слушаем" метод send() для установки нового значения $line;
        fwrite($f, $line);
    }
}
$log = createLog($file);
$log->send("First");
$log->send("Second");
$log->send("Third");

Пример последовательного вызова функций Аналог модуля Step в NodeJS

function step1() {
    $f = fopen("file.txt", 'r');
    while ($line = fgets($f)) {
        processLine($line);
        yield true;
    }
}
function step2() {
    $f = fopen("file2.txt", 'r');
    while ($line = fgets($f)) {
        processLine($line);
        yield true;
    }
}
function step3() {
    $f = fsockopen("www.example.com", 80);
    stream_set_blocking($f, false);
    $headers = "GET / HTTP/1.1\r\n";
    $headers .= "Host: www.example.com\r\n";
    $headers .= "Connection: Close\r\n\r\n";
    fwrite($f, $headers);
    $body = '';
    while (!feof($f)) {
        $body .= fread($f, 8192);
        yield true;
    }
    processBody($body);
}
 
// 3 потока (step) имеют схожий функционал - выбрасывают true, тем самым давая сигнал, что он еще занят
 
function runner(array $steps) {                    
    while (true) {                                                # снова бесконечный цикл, в котором перебираем потоки
        foreach ($steps as $key => $step) {  
             $step->next();                                    # возобновляем работу потока с с момента последнего yield
             if (!$step->valid()) {                           # проверяем, завершился ли поток и завершаем (удаляем) его
                 unset($steps[$key]);
             }
        }
        if (empty($steps)) return;                      # если потоков нет - завершаем работу
    }
}
runner(array(step1(), step2(), step3()));