Req #77343 [NEW]: Method to reply to yielded data from within a foreach loop
| From: | stephan dot soller at helionweb dot de | Date: | Sun, 23 Dec 2018 21:41:34 +0000 |
| Subject: | Req #77343 [NEW]: Method to reply to yielded data from within a foreach loop | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-218594@lists.php.net to get a copy of this message | ||
From: stephan dot soller at helionweb dot de
Operating system:
PHP version: master-Git-2018-12-23 (Git)
Package: *General Issues
Bug Type: Feature/Change Request
Bug description:Method to reply to yielded data from within a foreach loop
Description:
------------
I'm not sure if this is the right place to post such a feature request
or if I should post an RFC in the wiki. Feel free to send me in the
right direction if this isn't the place.
Right now generators a good for emitting data (
yield $data) or for
receiving data ($generator->send($data) on the outside and `$data =
yield` in the generator). In my current project I stumbled upon a use
case where I wanted to do both at the same time: Fetching mails via
POP3, yielding them and if the outside loop processed the mail
successfully the mail should be deleted.
I found the Generator::send() method in the documentation and coded
something like this:
function fetch_mails(){
// for each mail
$action = yield $mail;
if ($action == "delete")
// delete mail
}
$mails = fetch_mails();
foreach($mails as $mail) {
if ( !process_mail($mail) )
$mails->send("delete");
}
The problem with this code is that foreach() and send() both advance the
generator. Meaning each time send() is called one mail was also skipped.
At first I thought it's a bug but after reading the docs I realized that
this is by design: You either emit data to the outside or you receive
data from the outside. Looks like there is nothing in place to do both
at the same time. Others seem to have similar problems
(https://stackoverflow.com/a/49314418,
https://markbakeruk.net/2016/10/08/php-generators-sending-gotchas/).
A solution is to not use foreach but a while loop. Also you need to emit
and receive values in tandem:
function fetch_mails(){
// for each mail
yield $mail;
$action = yield;
if ($action == "delete")
// delete mail
}
$mails = fetch_mails();
while($mails->valid()) {
$mail = $mails->current();
process_mail($mail) ? $mails->next() : $mails->send("delete");
}
But it's rather difficult to figure out what's going on here and it's
easy to break. So I dropped the generator and used it with a callback
instead (with all the associated pros and cons).
What I really wanted is a way to provide the return values of the
yield statement from within a foreach loop. Without advancing the
iterator like send() does. I looked at the PHP source code and added a
small Generator::reply() method that does the same as
Generator::send() but without advancing the generator. The patch is
attached and I wrote it based on the current PHP git master. With it the
following code works:
function fetch_mails(){
// for each mail
$action = yield $mail;
if ($action == "delete")
// delete mail
}
$mails = fetch_mails();
foreach($mails as $mail) {
if ( !process_mail($mail) )
$mails->reply("delete");
}
I usually wouldn't have taken the time to post this. But to me this
seemed like a useful pattern. You can use generators to encapsulate
complex flow control. But sometimes you have to steer the generator from
the outside. My current use case is a rather simple one (just delete the
mail or not) but this could be a useful feature for complex iterations
(e.g. of graphs, trees, file formats, network protocols). Similar to the
C function nftw() (new file tree walk) where you can skip siblings or
subtrees.
There are also details I'm not sure about:
a) The name. ´reply()` simply was the first idea that came to mind.
b) reply() simply sets the return value of the current yield
expression in the generator. So it can be called multiple times during
the same iteration. Each call overwrites the previously set value. This
makes the code simple but this might give users wrong ideas. Some maybe
start to think that the generator gets advanced when you call reply()
twice or more often. Maybe it would be best to throw an exception when
reply() is called more than once per iteration. But that would require
resetting the send_target slot to a known value when advancing the
generator (e.g. UNDEF).
I can write up an RFC in the wiki if this is the way it's done. But I
think someone else should look at it and decide if it's useful or not.
If deemed useful I can also implement it properly (I think) but I still
have to read up on PHPs internal memory management.
--
Edit bug report at https://bugs.php.net/bug.php?id=77343&edit=1
--
Try a snapshot (PHP 5.4): https://bugs.php.net/fix.php?id=77343&r=trysnapshot54
Try a snapshot (PHP 5.5): https://bugs.php.net/fix.php?id=77343&r=trysnapshot55
Try a snapshot (trunk): https://bugs.php.net/fix.php?id=77343&r=trysnapshottrunk
Fixed in SVN: https://bugs.php.net/fix.php?id=77343&r=fixed
Fixed in release: https://bugs.php.net/fix.php?id=77343&r=alreadyfixed
Need backtrace: https://bugs.php.net/fix.php?id=77343&r=needtrace
Need Reproduce Script: https://bugs.php.net/fix.php?id=77343&r=needscript
Try newer version: https://bugs.php.net/fix.php?id=77343&r=oldversion
Not developer issue: https://bugs.php.net/fix.php?id=77343&r=support
Expected behavior: https://bugs.php.net/fix.php?id=77343&r=notwrong
Not enough info: https://bugs.php.net/fix.php?id=77343&r=notenoughinfo
Submitted twice: https://bugs.php.net/fix.php?id=77343&r=submittedtwice
register_globals: https://bugs.php.net/fix.php?id=77343&r=globals
PHP 4 support discontinued: https://bugs.php.net/fix.php?id=77343&r=php4
Daylight Savings: https://bugs.php.net/fix.php?id=77343&r=dst
IIS Stability: https://bugs.php.net/fix.php?id=77343&r=isapi
Install GNU Sed: https://bugs.php.net/fix.php?id=77343&r=gnused
Floating point limitations: https://bugs.php.net/fix.php?id=77343&r=float
No Zend Extensions: https://bugs.php.net/fix.php?id=77343&r=nozend
MySQL Configuration Error: https://bugs.php.net/fix.php?id=77343&r=mysqlcfg