Lay András
Lay András

Reputation: 855

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?

Upvotes: 22

Views: 14861

Answers (3)

Nuryagdy Mustapayev
Nuryagdy Mustapayev

Reputation: 785

solutions provided here does not work if you need to iterate more than once.

so I used iterator_to_array function to convert it to array;

$items = iterator_to_array($items);

Upvotes: 0

donquixote
donquixote

Reputation: 5491

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.

Upvotes: 14

Luca Rainone
Luca Rainone

Reputation: 16468

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();
}

Upvotes: 27

Related Questions