Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP generator yield the first value, then iterate over the rest

I have this code:

<?php

function generator() {
    yield 'First value';
    for ($i = 1; $i <= 3; $i++) {
        yield $i;
    }
}

$gen = generator();

$first = $gen->current();

echo $first . '<br/>';

//$gen->next();

foreach ($gen as $value) {
    echo $value . '<br/>';
}

This outputs:

First value
First value
1
2
3

I need the 'First value' to yielding only once. If i uncomment $gen->next() line, fatal error occured:

Fatal error: Uncaught exception 'Exception' with message 'Cannot rewind a generator that was already run'

How can I solve this?

like image 282
Lay András Avatar asked Oct 29 '15 22:10

Lay András


People also ask

How do I yield in PHP?

The yield keyword is used to create a generator function. Generator functions act as iterators which can be looped through with a foreach loop. The value given by the yield keyword is used as a value in one of the iterations of the loop.

What is PHP yield statement?

In its simplest form, a yield statement looks much like a return statement, except that instead of stopping execution of the function and returning, yield instead provides a value to the code looping over the generator and pauses execution of the generator function.

How is generator used in PHP?

A generator in PHP is a function that allows us to iterate over data without needing to build an array in memory. Unlike a standard function, which can return only a single value, a generator can yield as many values as it needs to.

What is a generator and how is it used in PHP?

A generator allows you to write code that uses foreach to iterate over a set of data without needing to build an array in memory, which may cause you to exceed a memory limit, or require a considerable amount of processing time to generate.


2 Answers

The problem is that the foreach try to reset (rewind) the Generator. But rewind() throws an exception if the generator is currently after the first yield.

So you should avoid the foreach and use a while instead

$gen = generator();

$first = $gen->current();

echo $first . '<br/>';
$gen->next();

while ($gen->valid()) {
    echo $gen->current() . '<br/>';
    $gen->next();
}
like image 170
Luca Rainone Avatar answered Oct 09 '22 00:10

Luca Rainone


chumkiu's answer is correct. Some additional ideas.

Proposal 0: remaining() decorator.

(This is the latest version I am adding here, but possibly the best)

PHP 7+:

function remaining(\Generator $generator) {
    yield from $generator;
}

PHP 5.5+ < 7:

function remaining(\Generator $generator) {
    for (; $generator->valid(); $generator->next()) {
        yield $generator->current();
    }
}

Usage (all PHP versions):

function foo() {
  for ($i = 0; $i < 5; ++$i) {
    yield $i;
  }
}

$gen = foo();
if (!$gen->valid()) {
  // Not even the first item exists.
  return;
}
$first = $gen->current();
$gen->next();

$values = [];
foreach (remaining($gen) as $value) {
  $values[] = $value;
}

There might be some indirection overhead. But semantically this is quite elegant I think.

Proposal 1: for() instead of while().

As a nice syntactic alternative, I propose using for() instead of while() to reduce clutter from the ->next() call and the initialization.

Simple version, without your initial value:

for ($gen = generator(); $gen->valid(); $gen->next()) {
  echo $gen->current();
}

With the initial value:

$gen = generator();

if (!$gen->valid()) {
    echo "Not even the first value exists.<br/>";
    return;
}

$first = $gen->current();

echo $first . '<br/>';
$gen->next();

for (; $gen->valid(); $gen->next()) {
    echo $gen->current() . '<br/>';
}

You could put the first $gen->next() into the for() statement, but I don't think this would add much readability.


A little benchmark I did locally (with PHP 5.6) showed that this version with for() or while() with explicit calls to ->next(), current() etc are slower than the implicit version with foreach(generator() as $value).

Proposal 2: Offset parameter in the generator() function

This only works if you have control over the generator function.

function generator($offset = 0) {
    if ($offset <= 0) {
        yield 'First value';
        $offset = 1;
    }
    for ($i = $offset; $i <= 3; $i++) {
        yield $i;
    }
}

foreach (generator() as $firstValue) {
  print "First: " . $firstValue . "\n";
  break;
}

foreach (generator(1) as value) {
  print $value . "\n";
}

This would mean that any initialization would run twice. Maybe not desirable.

Also it allows calls like generator(9999) with really high skip numbers. E.g. someone could use this to process the generator sequence in chunks. But starting from 0 each time and then skipping a huge number of items seems really a bad idea performance-wise. E.g. if the data is coming from a file, and skipping means to read + ignore the first 9999 lines of the file.

like image 44
donquixote Avatar answered Oct 08 '22 23:10

donquixote