20

I have a method which takes a generator plus some additional parameters and returns a new generator:

function merge(\Generator $carry, array $additional)
{
    foreach ( $carry as $item ) {
        yield $item;
    }
    foreach ( $additional as $item ) {
        yield $item;
    }
}

The usual use case for this function is similar to this:

function source()
{
    for ( $i = 0; $i < 3; $i++ ) {
        yield $i;
    }
}

foreach ( merge(source(), [4, 5]) as $item ) {
    var_dump($item);
}

But the problem is that sometimes I need to pass empty source to the merge method. Ideally I would like to be able to do something like this:

merge(\Generator::getEmpty(), [4, 5]);

Which is exactly how I would do in C# (there is a IEnumerable<T>.Empty property). But I don't see any kind of empty generator in the manual.

I've managed to work around this (for now) by using this function:

function sourceEmpty()
{
    if ( false ) {
        yield;
    }
}

And this works. The code:

foreach ( merge(sourceEmpty(), [4, 5]) as $item ) {
    var_dump($item);
}

correctly outputs:

int(4)
int(5)

But this is obviously not an ideal solution. What would be the proper way of passing an empty generator to the merge method?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Maciej Sz
  • 11,151
  • 7
  • 40
  • 56
  • Why use a `merge`, if the generator is empty? why not foreach the array directly? Anyway, a simple fix would be to swap the arguments (put the array first) and set the `Generator` argument to a default value of `null`, making it optional – Elias Van Ootegem Aug 21 '14 at 14:17
  • or just use an empty `array()` as empty generator – Sirac Aug 21 '14 at 14:19
  • @EliasVanOotegem The API of the `merge` method cannot be changed. It's a part of a much larger recursive array-reduce like algorithm, thus it has to stay compatible with PHP's `array_reduce` function. Besides other code already uses this method so it would be backward compatibility break. – Maciej Sz Aug 21 '14 at 14:20
  • @Sirac Dropping the `\Generator` type hint (if this is what you suggesting - otherwise `array` would not work) is a dirty hack. I want to keep it clean. – Maciej Sz Aug 21 '14 at 14:25
  • In case anyone is interested in the performance impact, here are the VLD opcodes of the answers in this question: https://3v4l.org/DpcJX/vld#output I did not profile and call the functions 10000000 times since the function call overhead variance is most likely higher than the actual difference between the functions, but it seems that `false && yield;` has the simplest opcode (even simpler than `if(false) yield;`). – SOFe Apr 13 '20 at 14:40

4 Answers4

29

Bit late, but needed an empty generator myself, and realized creating one is actually quite easy...

function empty_generator(): Generator
{
    yield from [];
}

Don't know if that's better than using the EmptyIterator, but this way you get exactly the same type as non-empty generators at least.

Svish
  • 152,914
  • 173
  • 462
  • 620
  • 1
    This is a neat solution too. In my case I'm gonna stick with the `new \EmptyIterator` as it doesn't require implementing boilerplate `empty_generator()` function, but this answer might be very useful for other use cases. – Maciej Sz Mar 27 '17 at 13:06
  • 1
    Well, in my case I didn't even bother with the boiler plate function, I just did `yield from []` right there – Svish Mar 27 '17 at 16:25
  • 1
    NOTE: This answer uses ["Generator Delegation"](https://wiki.php.net/rfc/generator-delegation), which was added in php 7.0. – ToolmakerSteve Mar 10 '20 at 01:15
4

Just for completeness, perhaps the least verbose answer so far:

function generator() {
    return; yield;
}

I just wondered about the same question and remembered an early description in the docs (which should be in at least semantically until today) that a generator function is any function with the yield keyword.

Now when the function returns before it yields, the generator should be empty.

And so it is.

Example on 3v4l.org: https://3v4l.org/iqaIY

hakre
  • 193,403
  • 52
  • 435
  • 836
  • Succinct, but IMHO non-obvious. As confirmed by the fact that you had to test it, to see if it actually worked :) – ToolmakerSteve Mar 10 '20 at 00:50
  • @ToolmakerSteve: Maybe that is more self-confirmation? I guess you clicked the link to see if it actually worked, correct? – hakre Mar 10 '20 at 06:55
  • `true or yield;` is actually less verbose. Or `1 or yield;` if you want to code golf. (use `1||yield` as a true expression if number of characters counts) – SOFe Jan 16 '21 at 06:28
3

I've found the solution:

Since \Generator extends \Iterator I can just change the method signature to this:

function merge(\Iterator $carry, array $additional) 
{
    // ...

This is input covariance thus it would break backward compatibility, but only if someone did extend the merge method. Any invocations will still work.

Now I can invoke the method with PHP's native EmptyIterator:

merge(new \EmptyIterator, [4, 5]);

And the usual generator also works:

merge(source(), [4, 5])
SOFe
  • 7,867
  • 4
  • 33
  • 61
Maciej Sz
  • 11,151
  • 7
  • 40
  • 56
  • 1
    You could further extend the type-hint to `Traversable`. `Iterator`, and `ArrayAccess` and some other interfaces all extend from the internal `Traversable` interface. Just for completeness (but I guess you already know this), hinting at a behavioural interface does make your code more error-prone. BTW: if it's a global object, and you're not in any particular namespace, the leading backslash can be left out. – Elias Van Ootegem Aug 22 '14 at 11:49
  • 1
    I'd just drop the typehints altogether and document both params as `@param \Traversable|array`. To allow usable with any kind of "thing" that can be iterated. – NikiC Feb 24 '15 at 20:05
  • 1
    Follow-on to @NikiC's comment: php 7.1 introduces [`iterable` type hint](https://wiki.php.net/rfc/iterable). So can say `function merge(iterable $carry, iterable $additional)`. – ToolmakerSteve Mar 10 '20 at 01:06
0

As explained in the official docs, you can create an in-line Generator instance, by using yield in an expression:

$empty = (yield);

That should work, but when I tried using that, I got a fatal error (yield expression can only be used in a function). Using null didn't help either:

$empty = (yield null); //error

So I guess you're stuck with the sourceEmpty function... it was the only thing I found that works... note that it will create a null value in the array you're iterating.
All the code was tested on PHP 5.5.9, BTW

The best fix I can come up with (seeing as compatibility is an issue) would be to make both arguments optional:

function merge(\Generator $carry = null, array $additional = array())
{
    if ($carry)
        foreach ($carry as $item)
            yield $item;
    foreach ($additional as $item)
        yield $item;
}
foreach(merge(null, [1,2]) as $item)
    var_dump($item);

This way, existing code won't brake, and instead of constructing an empty generator, passing null will work just fine, too.

Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
  • I actually did test the inline expression you provided before asking the question, but as you said - it doesn't work. The `sourceEmpty` however does _not_ behave like you described. The output is correct (at least for me, I'm on 5.5.15) - there is no additional preceding NULL. That's why it is the only _working_ solution so far. Maybe it was a bug fixed somewhere between our versions. – Maciej Sz Aug 21 '14 at 15:06
  • 1
    @MaciejSz: Can't find anything in the changelogs to suggest that such a bug was fixed. There have been some minor changes to the `Iterator` interface implementations, and to the generators, though. Either way, making both arguments optional (though hacky) would be the closest you can get to a function-call-less solution. Generators are quite new in PHP, and as is the case with many of PHP's latest trinkets, they're not really...erm... the best implementations of the concept. – Elias Van Ootegem Aug 21 '14 at 15:18
  • Thanks for your input, I've found a bit cleaner solution if you are interested. You are totally right about the implementation of PHP's trinkets though :) – Maciej Sz Aug 22 '14 at 11:42
  • 3
    Expressions containing `yield` do not create an inline generator, but are used for retrieving a value sent to the generator. – md2perpe Aug 04 '15 at 15:09
  • I am unable to locate the exact paragraph from the PHP docs you are referencing. Could you quote? – SOFe Aug 18 '18 at 17:15
  • @SOFe [Generator syntax, a bit below "yield keyword" section](https://www.php.net/manual/en/language.generators.syntax.php#control-structures.yield): *"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 in PHP 5. ... The parenthetical restrictions do not apply in PHP 7."* – ToolmakerSteve Mar 10 '20 at 00:41
  • @SOFe - or if you were asking about Elias' *"you can create an in-line Generator instance, by using yield in an expression"*: probably the documentation has been improved since this answer; that sentence is a mis-statement of what `yield` does. `yield` is used *within a function*, to turn *that function* into a Generator; it isn't meaningful to do what Elias was attempting to do in his first two code snippets (he was trying to create an empty Generator in a single line of code, without defining a separate function to be the Generator). – ToolmakerSteve Mar 10 '20 at 00:48
  • Downvoted as this answer does not really reply to the question directly. The anwer is eventually `if(false) yield;` but explained in a much more complicated way than it needs to be. – SOFe Apr 13 '20 at 14:36