Reputation: 26137
I want to write example codes step by step how to separate tasks in a generator and move them into 2 or more generators in order to achieve cooperative multitasking between them. You can find all of my tests about this here.
Generators are logical in some way, but I got stucked by a single step, which I cannot explain why it works this way:
The generator:
$spy = new Object();
$spy->tasks = array();
$createGenerator = function ($i1) use ($spy) {
yield; //(* -> task 1)
$spy->tasks[] = $i1;
yield($i1); //(task 1 -> *)
$i1 = yield; //(* -> task 2)
//task 2
$i2 = $i1 + 1;
$spy->tasks[] = $i2;
yield($i2); //(task 2 -> *)
$i2 = yield; //(* -> task 3)
$i3 = $i2 + 1;
$spy->tasks[] = $i3;
yield($i3); //(task 3 -> *)
$i3 = yield; //(* -> task 4)
$i4 = $i3 + 1;
$spy->tasks[] = $i4;
yield($i4); //(task 4 -> *)
$i4 = yield; //(* -> task 5)
$i5 = $i4 + 1;
$spy->tasks[] = $i5;
yield($i5); //(task 5 -> *)
};
The test I waited to succeed, but it failed:
/** @var Generator $generator */
$generator = $createGenerator(1);
$i1 = $generator->send(null);
$generator->send($i1);
$i2 = $generator->send(null);
$generator->send($i2);
$i3 = $generator->send(null);
$generator->send($i3);
$i4 = $generator->send(null);
$generator->send($i4);
$i5 = $generator->send(null);
$this->assertSame($spy->tasks, array(1, 2, 3, 4, 5));
$this->assertSame(array($i1, $i2, $i3, $i4, $i5), array(1, 2, 3, 4, 5));
The test that unexpectedly succeeds:
/** @var Generator $generator */
$generator = $createGenerator(1);
$i1 = $generator->send(null);
$generator->send(null); //blank sends needed to skip the yield-yield gaps
$i2 = $generator->send($i1);
$generator->send(null);
$i3 = $generator->send($i2);
$generator->send(null);
$i4 = $generator->send($i3);
$generator->send(null);
$i5 = $generator->send($i4);
$this->assertSame($spy->tasks, array(1, 2, 3, 4, 5));
$this->assertSame(array($i1, $i2, $i3, $i4, $i5), array(1, 2, 3, 4, 5));
Can you explain me this odd behavior of generators by double yield
?
Conclusion:
The send()
always runs the code from the input of a yield
to the output of the next yield
. So by running a Generator
with send()
, it always begins with an input, that's why you cannot get the output of the first yield
with a send()
, and that's why you always got a null
return value by the last send()
, before the Generator
goes to an invalid state... Sadly the PHP manual has a lack of this information...
Upvotes: 3
Views: 1644
Reputation: 1629
This has confused me for several hours now, but I've managed to find out what exactly is going on and how send
works. The important conclusions are:
send
) or reading from (via foreach
or current()
) the Generator first, it will run to the first yield
to get "initialised" and then perform your send/receive at that yield
. If your first operation is calling next()
, it will run all the way to the second yield
.yield
will be both the exit and entry point for the next read/send operation.send()
performs both a send and a read operation in that order, regardless of what you do with the result. This means when you call send()
, the Generator will move to the next yield
while handling your call.With this in mind, let's walk through your test:
$createGenerator(1)
just makes the generator and doesn't run anything, as expected.$i1 = $generator->send(null);
performs a send first, then a read:
yield
.null
you sent is this yield
's value if you were to assign it.yield
, adding $i1
to your task list and then handing execution back, with the yielded value 1
.$i1
.$generator->send($i1);
now sends this value back into the Generator, again performing a send, then advancing the Generator to the next yield
, and reading the yielded value:
yield($i1);
, which previously provided the value 1
. This was the exit point and now becomes the entry point receiving the sent value (second conclusion above).yield
.yield
doesn't provide a value, so null
is yielded.1
was discarded rather than stored.$i2 = $generator->send(null);
now sends in a null
, which again results in a send and read operation, in that order, advancing the Generator in the process:
$i1 = yield; //(* -> task 2)
is the current yield
, which was previously the exit point and now becomes the entry point. Since we just sent in null
, that's what is stored in $i1
.null + 1
(so 1
) in $i2
, storing that in your task list and then yielding it.1
is now stored in your test code's $i2
. At this point, the previous mistake has leaked out into the test code.1
, which will be discarded by the yield
statement that sent it out, then the next yield
will store the null
you send in next and repeat everything. Your resulting array will be [1, 1, 1, 1, 1]
.Your working test works because it moves the sent value one step further, to the yield
that actually stores what you send. @GhostGambler's Generator works for the opposite reason, it moves the assignment inside your Generator one step closer, to the yield
that actually receives the value from your test code.
To illustrate these inner workings, consider the following adapted example:
$createGenerator = function ($i) {
$in1 = yield("out".$i++); //(* -> task 1)
echo "in1: $in1";
$in2 = yield("out".$i++); //(task 1 -> *)
echo "in2: $in2";
$in3 = yield("out".$i++); //(* -> task 2)
echo "in3: $in3";
$in4 = yield("out".$i++); //(task 2 -> *)
echo "in4: $in4";
echo "\nGenerator done with i=$i";
};
$generator = $createGenerator(1);
$out = $generator->send("in1");
echo "\nout1: ";var_dump($out);
$out = $generator->send("in2");
echo "\nout2: ";var_dump($out);
$out = $generator->send("in3");
echo "\nout3: ";var_dump($out);
$out = $generator->send("in4");
echo "\nout4: ";var_dump($out);
Which creates the following output:
in1: in1
out1: string(4) "out2"
in2: in2
out2: string(4) "out3"
in3: in3
out3: string(4) "out4"
in4: in4
Generator done with i=5
out4: NULL
Notice that "out1" (the first yielded value) is never captured because the first operation on the Generator is a send. Consequently, the last read operation fails because the Generator only yields 4 values and we're trying to access the fifth yielded value (since we implicitly discarded the first).
One last gotcha is next()
, which is implicitly called at the end of each iteration of a foreach
loop. That one is essentially identical to send(null)
: It sends nothing into the Generator, makes it run to the next yield
, and discards the next yielded value (although in a foreach
loop, current()
is then also called to pick that discarded value up and store it inside the as
variable). This makes it rather tricky to use send()
inside a foreach
.
Upvotes: 2
Reputation: 6654
Working example of a generator for your test:
$spy = new stdClass();
$spy->tasks = array();
$createGenerator = function ($i1) use ($spy) {
yield;
$spy->tasks[] = $i1;
$i1 = (yield $i1);
yield;
$i2 = $i1 + 1;
$spy->tasks[] = $i2;
$i2 = (yield $i2);
yield;
$i3 = $i2 + 1;
$spy->tasks[] = $i3;
$i3 = (yield $i3);
yield;
$i4 = $i3 + 1;
$spy->tasks[] = $i4;
$i4 = (yield $i4);
yield;
$i5 = $i4 + 1;
$spy->tasks[] = $i5;
(yield $i5);
};
Your test:
$generator = $createGenerator(1);
$i1 = $generator->send(null);
$generator->send($i1);
$i2 = $generator->send(null);
$generator->send($i2);
$i3 = $generator->send(null);
$generator->send($i3);
$i4 = $generator->send(null);
$generator->send($i4);
$i5 = $generator->send(null);
print_r($spy);
for ($i = 1; $i <= 5; ++$i) {
echo ${'i'.$i} . "\n";
}
This gives the desired result:
stdClass Object
(
[tasks] => Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
)
)
1
2
3
4
5
See the manual on the send method for further information, which sums up all there is to say about how send
works:
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.
You should already know what yield
does.
To fully understand the interaction between the generator and your test, it should help you to write down (with a pen on a piece of paper) each step in the execution flow of your source code.
Also notice the caution box on yield in the manual:
Caution
If you use yield in an expression context (for example, on the right hand side of an assignment), you must surround the yield statement with parentheses. For example, this is valid:
$data = (yield $value);
But this is not, and will result in a parse error:
$data = yield $value;
This syntax may be used in conjunction with the Generator::send() method.
Upvotes: 2