Early return in functions and yield
Asked Answered
C

1

6

I'm not sure what the correct way is to early return in a function when using the yield keyword.

Is a "return void" return; the correct return value? Because the return type is specified to be iterable. Although it works without any error, returning void if the type hint would be int would lead to an error. So is iterable type hint an exception to still accept a void return statement although the type hint indicates something iterable should be returned?

See the example below to clarify my question:

class Collection
{
    public $items;
}

class OrderedCollection extends Collection
{
    public $orderedItems;
}

class Test
{
    public function getCollectionItems(Collection $collection): iterable
    {
        if ($collection instanceof OrderedCollection) {
            yield from $collection->orderedItems;
            return; // What should I return here to early exit the function?
        }

        yield from $collection->items;
    }
}
Carillon answered 23/5, 2020 at 10:30 Comment(3)
You could change it to use else { yield from $collection->items; };Soakage
@NigelRen this would be an option. But I'm interested if there is a solution without else. :-)Carillon
@NigelRen that would not have been early exitSusceptive
C
1

Generator functions should have a return type of \Generator or a supertype†.


Generator functions don't "return" what they're yielding, they "return" a "yielder" if you want to think about it like that. I believe that much of the confusion stems from referring to generator functions as generators.

A better way to think about it is that a generator function outputs a Generator object.

So we have two terms:

Another confusing aspect is that we use the return type syntax to denote that the function will output a Generator. In reality nothing can be literally returned from a generator function. The return; statement in the context of a generator function is more like an exit; for the function scope.

(The pedantic amongst you best not mention the analogous values returned by exit(1) in this discussion. 🙏 )


I think this example demonstrates all of this decently:

function test(): \Generator {
    yield 'test';
    return "This text vanishes into the abyss."; // return values aren't a thing
    return; // instead, this "void" is the only return statement that makes sense
    yield 'Execution never reaches this text.';
}

$test = test(); // this is a \Generator object

foreach($test as $item) { // \Generator objects are iterable
    echo $item; // prints 'test' and nothing else
}

† In your specific case (setting the return type to iterator) the code works fine, presumably because a Generator is some form of iterator implementation. The PHP manual and errors use the word supertype to describe allowed return types, but I guess iterator (which I consider a subtype) applies as well.

By thinking of this iterator as more of an "iterable" interface, it might be more clear what generator functions' return type should be: Something you can iterate over, which is most accurately, a Generator object.

I recommend against using the return type iterator for a generator function since it's not as clear and it may obfuscate typos in the function until runtime. I won't spell out an edge case example, but you can "smell" it. Anyway, it's a simple rule for me: If a function contains yield; I'm going to return type it \Generator.


Trivia:

  • The yield statement itself is what makes the function a generator function. This means that if you use yield anywhere in the function, executed or not, the function will be "compiled" as a generator function. (This doesn't include // yield in comments of course.) This means that declaring a return type isn't required for a generator function, but as always, is best practice.
Commendatory answered 22/8, 2024 at 17:44 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.