Req #77343 [Opn]: Method to reply to yielded data from within a foreach loop

From: Date: Sun, 23 Dec 2018 22:56:22 +0000
Subject: Req #77343 [Opn]: Method to reply to yielded data from within a foreach loop
References: 1  Groups: php.bugs 
Request: Send a blank email to php-bugs+get-218595@lists.php.net to get a copy of this message
Edit report at https://bugs.php.net/bug.php?id=77343&edit=1 ID: 77343 Updated by: requinix@php.net Reported by: stephan dot soller at helionweb dot de Summary: Method to reply to yielded data from within a foreach loop Status: Open Type: Feature/Change Request Package: *General Issues PHP Version: master-Git-2018-12-23 (Git) Block user comment: N Private report: N New Comment: You should post about this on the internals list: many more people will see it that way, and it creates a more suitable place to talk about it than here on this simple bug tracker. Previous Comments: ------------------------------------------------------------------------ [2018-12-23 21:41:34] stephan dot soller at helionweb dot de 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 this bug report at https://bugs.php.net/bug.php?id=77343&edit=1

« previous php.bugs (#218595) next »