Как работает 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()));