Reading time:

Reinventing the Wheel: PHP Generators

First thing first. How a generator works?

Starting back at C

Let's create a function that each time we call it we get the next number of the fibonacci sequence.
int fibonacci()
{
    static int a = 0;
    static int b = 1;
    int aux = b;
    b = a + b;
    a = aux;
    return a;
}
If we call fibonacci(), the first time we'll get 1, the second time 1, the third 2, the fourth 3, and so on... This happens because we declared variables a, b to be static. This means that they mantain the value after the function returns. Normally, what happens (if we don't declare a variable as static) is that the variables inside the function don't mantain the values of the last execution.

First generator for PHP

The equivalent function in PHP is pretty similar to C's approach.


function fibonacci()
{
    static $a = 0;
    static $b = 1;
    $aux = $b;
    $b = $a + $b;
    $a = $aux;
    return $a;
}

$out = [];

for ($i = 1; $i <= 10; $i++) {
    $out[] = fibonacci();
}

echo implode(', ', $out) . "\n";

/*
Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
*/
Let's compare this to the real PHP version using yield.


function fibonacci($N)
{
    $a = 0;
    $b = 1;
    for ($i = 0; $i < $N; $i++) {
        $aux = $b;
        $b = $a + $b;
        $a = $aux;
        yield $a;
    }
}

$out = [];

foreach (fibonacci(10) as $fib) {
    $out[] = $fib;
}

echo implode(', ', $out) . "\n";

/*
Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
*/

Creating a custom version of PHP yield

This is my own version using the library parallel and channels (probably uses yield internally).


class MyGenerator implements Iterator
{
    private $chan;
    private $current;
    private $iteratorFn;
    private $runtime;
    private $key = -1;
    private $valid = true;

    public function __construct($iteratorFn)
    {
        $this->iteratorFn = $iteratorFn;
        $this->runtime = new \parallel\Runtime();
        $channel = new \parallel\Channel();

        $this->runtime->run(function() use ($iteratorFn, $channel) {
            $iteratorFn(function ($val) use ($channel) {
                $channel->send($val);
            });
            $channel->close();
        });

        $this->chan = $channel;
        $this->next();
    }

    public function current()
    {
        return $this->current;
    }

    public function next()
    {
        try {
            ++$this->key;
            $val = $this->chan->recv();
            $this->current = $val;
        } catch (\parallel\Channel\Error\Closed $e) {
            $this->valid = false;
        }
        return $this->current;
    }

    public function key() {return $this->key;}
    public function valid() {return $this->valid;}
    public function rewind() {}
}


function fibonacci($N)
{
    return new MyGenerator(function ($yield) use ($N) {
        $a = 0;
        $b = 1;
        for ($i = 0; $i < $N; $i++) {
            $aux = $b;
            $b = $a + $b;
            $a = $aux;
            $yield($a);
        }
    });
}

$out = [];

foreach (fibonacci(10) as $fib) {
    $out[] = $fib;
}

echo implode(', ', $out) . "\n";

Performance comparison: PHP vs Custom

Tested code
for ($i = 0; $i < 1000; ++$i) {
    foreach (fibonacci(100) as $fib) {
        $out[] = $fib;
    }
}
yield version
real    0m0,083s
user    0m0,059s
sys     0m0,023s
MyGenerator version
real    0m2,138s
user    0m1,426s
sys     0m1,363s
So, it's aproximately 26 times slower :-)