Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Generator::send work?

Tags:

generator

php

Usually I'm not confused about language constructs, but I can't make heads or tails of what's going on here.

<?php

function action() {
    for($i=0; $i<10; ++$i) {
        $ans = (yield expensive($i));
        echo "action $ans\n";
    }
}

function expensive($i) {
    return $i*2;
}

$gen = action();
foreach($gen as $x) {
    echo "loop $x\n";
    $gen->send($x);
}

Prints:

loop 0
action 0
action 
loop 4
action 4
action 
loop 8
action 8
action 
loop 12
action 12
action 
loop 16
action 16
action 

So every 2nd iteration of my loop is skipped, and I'm getting NULL for $ans periodically. What??

I thought $ans would receive the result of $gen->send, and if I didn't send anything before the next yield, then $ans would be null, but I always send something on each iteration, so what's is going on here?

like image 716
mpen Avatar asked Jun 14 '16 16:06

mpen


2 Answers

It's a documentation problem. Here's what a PHP developer wrote on a bug report:

next() and send() both advance the generator. That's how generators work. Using next(), explicitly or implicitly, means there's no way to pass a value back through the yield and thus the code will get null - just like when trying to get the return value from a function that doesn't return anything.

In other words, you can't use send() inside a foreach and expect meaningful results.


Actual foreach call next() after each iteration.

/** @return Generator */
function action() {
    for ($i = 0; $i < 5; $i += 1) {
        $answer = (yield $i * 2);
        echo "received: $answer\n";
    }
}


$gen = action();
while ($gen->valid()) {
    $x = $gen->current();
    echo "sending $x\n";
    $gen->send($x);
    $gen->next();
}

Now that we added it, the code starts to misbehave again:

sending 0
received: 0
received: 
sending 4
received: 4
received: 
sending 8
received: 8

If we remove the offending next() the code works as one can expect.

$gen = action();
while ($gen->valid()) {
    $x = $gen->current();
    echo "sending $x\n";
    $gen->send($x);
    //$gen->next();
}

Outputs:

sending 0
received: 0
sending 2
received: 2
sending 4
received: 4
sending 6
received: 6
sending 8
received: 8

Sounds like a bug for me. Even HHVM fails with a fatal error.

like image 131
sanmai Avatar answered Dec 09 '22 01:12

sanmai


I think the 'foreach' is messing things up. When the foreach loop starts, an iterator is created, and I guess it can't handle the fact that I'm injecting new things into the generator.

This:

<?php

/**
 * @return Generator
 */
function action() {
    for($i=0; $i<10; ++$i) {
        $ans = (yield expensive($i));
        echo "action $ans\n";
    }
}

function expensive($i) {
    return $i*2;
}

$gen = action();
while($gen->valid()) {
    $x = $gen->current();
    echo "loop $x\n";
    $gen->send($x);
}

Prints what I'd expect:

loop 0
action 0
loop 2
action 2
loop 4
action 4
loop 6
action 6
loop 8
action 8
loop 10
action 10
loop 12
action 12
loop 14
action 14
loop 16
action 16
loop 18
action 18

Things get weird again though if you send more than once per loop:

<?php

/**
 * @return Generator
 */
function action() {
    for($i=0; $i<10; ++$i) {
        $ans = (yield expensive($i));
        echo "action $ans\n";
    }
}

function expensive($i) {
    echo "expensive $i\n";
    return $i;
}

$gen = action();
while($gen->valid()) {
    $x = $gen->current();
    echo "loop $x\n";
    $gen->send($x);
    $gen->send($x);
}

Prints:

expensive 0
loop 0
action 0
expensive 1
action 0
expensive 2
loop 2
action 2
expensive 3
action 2
expensive 4
loop 4
action 4
expensive 5
action 4
expensive 6
loop 6
action 6
expensive 7
action 6
expensive 8
loop 8
action 8
expensive 9
action 8

I think what's happening here is that send is causing action to iterate twice for every one while iteration. If we remove the two sends() then we get stuck in an infinite loop. So... send() is advancing the iterator, whereas current() does not. And I think this explains what was going on with the foreach loop too -- both the foreach and send() were advancing the iterator, which is why every other result was being skipped!

like image 20
mpen Avatar answered Dec 09 '22 01:12

mpen