In PHP: what is the difference between "return", "yield", "yield from" and mixing both yield and return in same function?
Asked Answered
P

2

16

The difference between return and yield seemed clear until I figured out there was also yield from and the possibility to combine both return and yield in the very same function!

My understanding of return was that everything after was not executed, right?

However:

function generate(): iterable {
    return [1, 2, 3];
}

foreach (generate() as $value) {
    echo $value;
}

Produces: "123"

But the following:

function generate(): iterable {
    return [1, 2, 3];
    yield;
}

foreach (generate() as $value) {
    echo $value;
}

Produces nothing! So that means yield is executed?

Is this a bug?

Photogravure answered 14/2, 2020 at 19:15 Comment(1)
var_dump(generate()->GetReturn());Dimorph
P
22

Return

Simply gives back a unique value to the caller.

Yield

Transform the current function/method to return a Generator, which will produce more than a unique value: every times yield is triggered, it gives the value to the caller, one at a time, traditionally using a foreach loop.

Yield + Return

Generators, in addition to generate values, can also, provide a unique returned value. That value won't be part of the looping around the generator, it must be accessed using Generator::getReturn() method.

Return + Yield

This could be seen as a bug, however, it is not.

They are two phases:

  1. From code to bytecode: during this phase, the generate() function is seen to contain the yield keyword, it is therefore marked as producing a Generator.
  2. Execution: Because the return happens to be before the yield, the generator doesn't have the chance to produce any value. However, the [1, 2, 3] array can be retrieved with Generator::getReturn().

A complete annotated example:

// Generate integers 1 and 2
function generateIntegers1And2(): Generator {
    yield 1;                                  // <--+   <--+   <--+
    yield 2;                                  //  <-+    <-+    <-+
}                                             //    |      |      |
                                              //    |      |      |
foreach (generateIntegers1And2() as $value) { //    |      |      |
    var_dump($value); // Shows 1, then 2          ->*      |      |
}                                                       // |      |
                                                        // |      |
function generateOuterYield(): Generator {              // |      |
    // Yields the generator *itself* returned by           |      |
    // generateIntegers1And2() not the actual values       |      |
    // generated by it.                                    |      |
    // This means we are producing here a generator        |      |
    // of generator of integers.                           |      |
    yield generateIntegers1And2();          // <-+         |      |
}                                             // |         |      |
                                              // |         |      |
foreach (generateOuterYield() as $value) {    // |         |      |
    var_dump($value);                       // ->*         |      |
    // The two levels of imbrication means we have         |      |
    // to loop once more to actually consume               |      |
    // generateIntegers1And2                               |      |
    foreach ($value as $val) {                          // |      |
        var_dump($val); // Shows 1, then 2               ->*      |
    }                                                          // |
}                                                              // |
                                                               // |
// A generator can just be returned as-is:                        |
function generateOuterReturn(): Generator {                    // |
    return generateIntegers1And2();                            // |
}                                                              // |
                                                               // |
// it doesn't change the way it is consumed                       |
foreach (generateOuterReturn() as $value) {                    // |
    var_dump($value); // Shows 1, then 2                          |
}                                                              // |
                                                               // |
function generateOuterYieldFrom(): Generator {                 // |
    // First yield values generated by generateIntegers1And2()    |
    yield from generateIntegers1And2();                        // *<---+
    // then yield integers 3                                           |
    yield 3;                                                     // <--+
    // and 4                                                           |
    yield 4;                                                     //  <-+
}                                                                //    |
                                                                 //    |
foreach (generateOuterYieldFrom() as $value) {                   //    |
    var_dump($value); // Shows 1, 2, 3 and 4                         ->*
}

function generateIntegers56AndReturn(): Generator {
    yield 5;                                                  // <---+
    yield 6;                                                  //  <--+
                                                              //     |
    return ["five", "six"];                       // <--+            |
}                                                 //    |            |
                                                  //    |            |
$gen = generateIntegers56AndReturn();             //    |            |
                                                  //    |            |
// Consume the values **yielded** by                    |            |
// generateIntegers56AndReturn()                        |            |
foreach ($gen as $value) {                        //    |            |
    var_dump($value); // Shows 5, then 6                |          ->*
}                                                 //    |
                                                  //    |
// Access the value **returned** by the generator       |
var_dump($gen->getReturn());                      //  ->*

function wtf(): Generator {
    return ["W", "T", "F", "!"];
    // Without the following line, PHP would complain with a TypeError:
    // Return value of wtf() must be an instance of Generator, array returned.
    // The presence of a yield keyword anywhere inside the function makes it a Generator.
    // However, since we return *before* reaching any *yield*, 42 is never yielded.
    // This is empty generator!
    yield 42;
}

$gen = wtf();

// This foreach loop is not entered!
foreach ($gen as $value) {
    var_dump($value);
}

// However, we can loop on the array *returned* by wtf():
foreach ($gen->getReturn() as $value) {
    echo $value; // Will print: WTF!
}
Photogravure answered 14/2, 2020 at 19:37 Comment(1)
The last example, the return "ends" the function execution, that is, the code doesnt achieve the yeld.Graphite
V
6

From the documentation:

Any function containing yield is a generator function.

So it doesn't matter whether the yield is executed, the parser sees it somewhere in the function definition and turns it into a generator.

If the function never executes the yield statement, then the generator doesn't produce any values. The value returned by return is ignored when you try to use the result. The documentation says:

Note:
In PHP 5, a generator could not return a value: doing so would result in a compile error. An empty return statement was valid syntax within a generator and it would terminate the generator. Since PHP 7.0, a generator can return values, which can be retrieved using Generator::getReturn().

So you could do:

$gen = generate();
foreach ($gen as $value) {
    echo $value;
}
print_r($gen->getReturn());
Vescuso answered 14/2, 2020 at 19:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.