ScotlandPHP

Синтаксис генераторов

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

Когда вызывается генератор, он возвращает объект, который можно итерировать. Когда вы итерируете этот объект (к примеру в цикле foreach), PHP вызывает функцию генератор каждый раз, когда вам нужно новое значение, после чего сохраняет состояние генератора и при следующем вызове возвращает следующее значение.

Когда все значения в генераторе закончились, функция просто завершит работу ничего не вернув. После этого основной код продолжит работу, аналогично как, например когда в массиве закончились элементы для перебора.

Замечание:

В PHP 5 генераторы не могут возвращать значений и попытка сделать это приведет к ошибке компиляции. В генераторе может присутствовать только пустой оператор return, для обозначения конца генерируемой последовательности. Начиная с PHP 7 генераторы могут возвращать значения, которые можно получить с помощью Generator::getReturn().

Ключевое слово yield

Самая суть генератора заключается в ключевом слове yield. В самом простом варианте, оператор "yield" можно рассматривать как оператор "return", за исключением того, что вместо прекращения работы функции, "yield" только приостанавливает ее выполнение и возвращает текущее значение, и при следующем вызове функции она возобновит выполнения с места, на котором прервалась.

Пример #1 Простой пример выдачи значений

<?php
function gen_one_to_three() {
    for (
$i 1$i <= 3$i++) {
        
// Обратите внимание, что $i сохраняет свое значение между вызовами.
        
yield $i;
    }
}

$generator gen_one_to_three();
foreach (
$generator as $value) {
    echo 
"$value\n";
}
?>

Результат выполнения данного примера:

1
2
3

Замечание:

Последовательность целых чисел будет связана с получаемыми от генератора значениями, как будто перебирается неассоциативный массив.

Предостережение

Если вы хотите использовать "yield" в выражениях (например в правой части оператора присвоения), в PHP вы должны убирать его в скобки. Корректный пример:

$data = (yield $value);

А вот это не правильно и вызовет ошибку разбора в PHP 5:

$data = yield $value;

В PHP 7 этого ограничения нет.

Этот синтаксис можно использовать в сочетании с методом Generator::send().

Получение значений с ключами

PHP поддерживает ассоциативные массивы, и генераторы не являются исключением. Так же как можно получать простые значения, как показано выше, вы можете получать значения с ключами.

Синтаксис получения ключ/значение очень похож на синтаксис ассоциативных массивов, как показано ниже.

Пример #2 Получение пар ключ/значение

<?php
/* $input содержит пары ключ/значение разделенные точкой с запятой */

$input = <<<'EOF'
1;PHP;Likes dollar signs
2;Python;Likes whitespace
3;Ruby;Likes blocks
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
    Likes dollar signs
2:
    Python
    Likes whitespace
3:
    Ruby
    Likes blocks
Предостережение

Также как и для одиночных значений, получение пар ключ/значение в выражениях требует оборачивания их в скобки:

$data = (yield $key => $value);

Получение NULL

Для получения NULL нужно вызвать "yield" без аргументов. Ключ сгенерируется автоматически.

Пример #3 Получение NULL

<?php
function gen_three_nulls() {
    foreach (
range(13) as $i) {
        
yield;
    }
}

var_dump(iterator_to_array(gen_three_nulls()));
?>

Результат выполнения данного примера:

array(3) {
  [0]=>
  NULL
  [1]=>
  NULL
  [2]=>
  NULL
}

Получение значения по ссылке

Генераторы могут отдавать значения по ссылке. Это делается так же, как описано в разделе "возвращение ссылки из функции", с помощью амперсанда (&) перед именем функции.

Пример #4 Получение значений по ссылке

<?php
function &gen_reference() {
    
$value 3;

    while (
$value 0) {
        
yield $value;
    }
}

/* Обратите внимание, что мы можем изменять $number в цикле, и
 * так как генератор возвращает ссылку, $value
 * в gen_reference() также изменится. */
foreach (gen_reference() as &$number) {
    echo (--
$number).'... ';
}
?>

Результат выполнения данного примера:

2... 1... 0... 

Делегирование генератора с помощью yield from

В PHP 7, делегирование генератора позволяет вам получать значения из другого генератора, объекта Traversable, или массива, используя yield from. Внешний генератор будет возвращать значения из внутреннего генератора, объекта или массива, до того момента, пока они их отдают, после чего продолжится выполнения внешнего генератора.

Если генератор используется с yield from, то выражение yield from также будет возвращать значения из внутреннего генератора.

Предостережение

Сохранение в массив (т.е. iterator_to_array())

yield from не сбрасывает ключи. Ключи, возвращенные из объекта Traversable или массива, сохранятся. Таким образом, некоторые значения, могут пересекаться по ключам с другими yield или yield from, что, при записи в массив, повлечет за собой перезапись уже записанных значений.

Общий случай, когда это имеет значение, это когда iterator_to_array() возвращает массив с ключами по умолчанию. В этом случае можно получить печальный результат. iterator_to_array() имеет второй параметр use_keys, который можно установить в FALSE, для генерации собственных ключей и игнорировании ключей, переданных из объекта Generator.

Пример #5 yield from с iterator_to_array()

<?php
function from() {
    
yield 1// ключ 0
    
yield 2// ключ 1
    
yield 3// ключ 2
}
function 
gen() {
    
yield 0// ключ 0
    
yield from from(); // ключи 0-2
    
yield 4// ключ 1
}
// Задайте false вторым параметром для получения массива [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 [34];
    
yield from new ArrayIterator([56]);
    
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 [34];
    
yield from new ArrayIterator([56]);
    
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
add a note add a note

User Contributed Notes 9 notes

up
74
Adil lhan (adilmedya at gmail dot com)
4 years ago
For example yield keyword with Fibonacci:

function getFibonacci()
{
    $i = 0;
    $k = 1; //first fibonacci value
    yield $k;
    while(true)
    {
        $k = $i + $k;
        $i = $k - $i;
        yield $k;       
    }
}

$y = 0;

foreach(getFibonacci() as $fibonacci)
{
    echo $fibonacci . "\n";
    $y++;   
    if($y > 30)
    {
        break; // infinite loop prevent
    }
}
up
17
info at boukeversteegh dot nl
2 years ago
[This comment replaces my previous comment]

You can use generators to do lazy loading of lists. You only compute the items that are actually used. However, when you want to load more items, how to cache the ones already loaded?

Here is how to do cached lazy loading with a generator:

<?php
class CachedGenerator {
    protected
$cache = [];
    protected
$generator = null;

    public function
__construct($generator) {
       
$this->generator = $generator;
    }

    public function
generator() {
        foreach(
$this->cache as $item) yield $item;

        while(
$this->generator->valid() ) {
           
$this->cache[] = $current = $this->generator->current();
           
$this->generator->next();
            yield
$current;
        }
    }
}
class
Foobar {
    protected
$loader = null;

    protected function
loadItems() {
        foreach(
range(0,10) as $i) {
           
usleep(200000);
            yield
$i;
        }
    }

    public function
getItems() {
       
$this->loader = $this->loader ?: new CachedGenerator($this->loadItems());
        return
$this->loader->generator();
    }
}

$f = new Foobar;

# First
print "First\n";
foreach(
$f->getItems() as $i) {
    print
$i . "\n";
    if(
$i == 5 ) {
        break;
    }
}

# Second (items 1-5 are cached, 6-10 are loaded)
print "Second\n";
foreach(
$f->getItems() as $i) {
    print
$i . "\n";
}

# Third (all items are cached and returned instantly)
print "Third\n";
foreach(
$f->getItems() as $i) {
    print
$i . "\n";
}
?>
up
12
Harun Yasar harunyasar at mail dot com
2 years ago
That is a simple fibonacci generator.

<?php
function fibonacci($item) {
   
$a = 0;
   
$b = 1;
    for (
$i = 0; $i < $item; $i++) {
        yield
$a;
       
$a = $b - $a;
       
$b = $a + $b;
    }
}

# give me the first ten fibonacci numbers
$fibo = fibonacci(10);
foreach (
$fibo as $value) {
    echo
"$value\n";
}
?>
up
6
zilvinas at kuusas dot lt
1 year ago
Do not call generator functions directly, that won't work.

<?php

function my_transform($value) {
   
var_dump($value);
    return
$value * 2;
}

function
my_function(array $values) {
    foreach (
$values as $value) {
        yield
my_transform($value);
    }
}

$data = [1, 5, 10];
// my_transform() won't be called inside my_function()
my_function($data);

# my_transform() will be called.
foreach (my_function($data) as $val) {
   
// ...
}
?>
up
4
Hayley Watson
1 year ago
If for some strange reason you need a generator that doesn't yield anything, an empty function doesn't work; the function needs a yield statement to be recognised as a generator.

<?php

function gndn()
{
}

foreach(
gndn() as $it)
{
    echo
'FNORD';
}

?>

But it's enough to have the yield syntactically present even if it's not reachable:

<?php

function gndn()
{
    if(
false) { yield; }
}

foreach(
gndn() as $it)
{
    echo
'FNORD';
}

?>
up
4
christophe dot maymard at gmail dot com
3 years ago
<?php
//Example of class implementing IteratorAggregate using generator

class ValueCollection implements IteratorAggregate
{
    private
$items = array();
   
    public function
addValue($item)
    {
       
$this->items[] = $item;
        return
$this;
    }
   
    public function
getIterator()
    {
        foreach (
$this->items as $item) {
            yield
$item;
        }
    }
}

//Initializes a collection
$collection = new ValueCollection();
$collection
       
->addValue('A string')
        ->
addValue(new stdClass())
        ->
addValue(NULL);

foreach (
$collection as $item) {
   
var_dump($item);
}
up
2
Shumeyko Dmitriy
3 years ago
This is little example of using generators with recursion. Used version of php is 5.5.5
[php]
<?php
define
("DS", DIRECTORY_SEPARATOR);
define ("ZERO_DEPTH", 0);
define ("DEPTHLESS", -1);
define ("OPEN_SUCCESS", True);
define ("END_OF_LIST", False);
define ("CURRENT_DIR", ".");
define ("PARENT_DIR", "..");

function
DirTreeTraversal($DirName, $MaxDepth = DEPTHLESS, $CurrDepth = ZERO_DEPTH)
{
  if ((
$MaxDepth === DEPTHLESS) || ($CurrDepth < $MaxDepth)) {
   
$DirHandle = opendir($DirName);
    if (
$DirHandle !== OPEN_SUCCESS) {
      try{
        while ((
$FileName = readdir($DirHandle)) !== END_OF_LIST) { //read all file in directory
         
if (($FileName != CURRENT_DIR) && ($FileName != PARENT_DIR)) {
           
$FullName = $DirName.$FileName;
            yield
$FullName;
            if(
is_dir($FullName)) { //include sub files and directories
             
$SubTrav = DirTreeTraversal($FullName.DS, $MaxDepth, ($CurrDepth + 1));
              foreach(
$SubTrav as $SubItem) yield $SubItem;
            }
          }
        }
      } finally {
       
closedir($DirHandle);
      }
    }
  }
}

$PathTrav = DirTreeTraversal("C:".DS, 2);
print
"<pre>";
foreach(
$PathTrav as $FileName) printf("%s\n", $FileName);
print
"</pre>";
[/
php]
up
-17
denshadewillspam at HOTMAIL dot com
3 years ago
Note that you can't use count() on generators.

/**
* @return integer[]
*/
function xrange() {
    for ($a = 0; $a < 10; $a++)
    {
        yield $a;
    }
}

function mycount(Traversable $traversable)
{
    $skip = 0;
    foreach($traversable as $skip)
    {
        $skip++;
    }
    return $skip;
}
echo "Count:" . count(xrange()). PHP_EOL;
echo "Count:" . mycount(xrange()). PHP_EOL;
up
-7
dejiakala at gmail dot com
3 years ago
Another Fibonacci sequence with yield keyword:

<?php

function getFibonacci($first, $second, $total) {
  yield
$first;
  yield
$second;
  for (
$i = 1, $total -= 2; $i <= $total; $i++) {
   
$sum = $first + $second;
   
$first = $second;
   
$second = $sum;
    yield
$sum;
  }
}

// Generate first 10 numbers of the Fibonacci sequence starting from 0, 1
foreach (getFibonacci(0, 1, 10) as $fibonacci) {
 
// 0 1 1 2 3 5 8 13 21 34
 
echo $fibonacci . " ";
}

?>
To Top