Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use PHP generators without foreach?

Tags:

generator

php

Here's a simple JavaScript generator (via: http://blog.carbonfive.com/2013/12/01/hanging-up-on-callbacks-generators-in-ecmascript-6/)

function* powGenerator() {
  var result = Math.pow(yield "a", yield "b");
  return result;
}

var g = powGenerator();
console.log(g.next().value);   // "a", from the first yield
console.log(g.next(10).value); // "b", from the second
console.log(g.next(2).value);  // 100, the result

I'm trying to model something similar with PHP but it's a bit of a headache.

<?php
function powGenerator() {
  return pow((yield 'a'), (yield 'b'));
}

Before I go further, I get this error in PHP

Fatal error: Generators cannot return values using "return"

Ok, so maybe I'll just use another yield to get the final value out? ...

<?php
function powGenerator() {
  yield pow((yield 'a'), (yield 'b'));
}

$g = powGenerator(); //=> Generator {#180}
echo $g->send(10);   //=> "b"
echo $g->send(2);    //=> 100

OK, so I got my value back, but there's two major issues here

  1. Where did my "a" go? — Notice in the JS example I was able to access the both the "a" and the "b" yielded values as well as the 100 final result.

  2. The generator is still not done! — I have to call send an additional time to complete the generator

    $g->valid();   //=> true
    $g->send('?'); //=> null
    $g->valid();   //=> false
    

From PHP Generator::send

public mixed Generator::send ( mixed $value )

Sends the given value to the generator as the result of the current yield expression and resumes execution of the generator.

If the generator is not at a yield expression when this method is called, it will first be let to advance to the first yield expression before sending the value. As such it is not necessary to "prime" PHP generators with a Generator::next() call (like it is done in Python).

Emphasis on "As such it is not necessary to "prime" PHP generators with a Generator::next()". OK, but what does that really mean? I don't have to "prime" it like the JavaScript example, but the first yielded value is also getting swallowed.

Can anyone explain how you're meant to step through generators without using a foreach?

like image 772
Mulan Avatar asked Oct 06 '15 01:10

Mulan


1 Answers

The first yielded value wasn't swallowed, you just never looked at it.

$g = powGenerator();
echo $g->current(); //a

You're then twice sending in values and resuming execution, $g->valid() is true after this because you haven't resumed after the third yield - the generator isn't complete and there may be more for it to do. Consider:

function powGenerator() {
    yield pow((yield 'a'), (yield 'b'));
    echo "Okay, finishing here now!\n";
}

$g = powGenerator();
echo $g->current(), "\n"; //a
echo $g->send(10), "\n";  //b
echo $g->send(2), "\n";   //100
$g->next();               // Resumes execution of the generator,
                          // which prints its own message and completes.
var_dump($g->valid());    //false

This'll output:

a
b
100
Okay, finishing here now!
bool(false)

Now in PHP 7 you can return from a generator.

function powGenerator() {
    return pow((yield 'a'), (yield 'b'));
    echo "This will never print.";
}

$g = powGenerator();
echo $g->current(), "\n"; //a
echo $g->send(10), "\n";  //b
echo $g->send(2), "\n";   // Prints just the newline, you're moving on
                          // to a return which you must get explicitly.
var_dump($g->valid());    // Generator complete, you're free to get the return.
echo $g->getReturn(), "\n";

Which outputs:

a
b

bool(false)
100

As for stepping through them without a foreach - Generator implements Iterator, so it's got appropriate methods to treat it as such: current, key, next, rewind, and valid. With the caveat that rewind will throw an exception if you call it on a generator that's already begun.

An example which does this and also demonstrates PHP 7's new generator delegation:

function letterGenerator() {
    yield from range('a', 'z');
}

$g = letterGenerator();

while ($g->valid()) {
    echo $g->current();
    $g->next();
}

Output:

abcdefghijklmnopqrstuvwxyz
like image 189
user3942918 Avatar answered Oct 12 '22 09:10

user3942918