0

Generators and coroutines — interview Q&A deep dive

Advanced5 min read·eng-09-009
interview

Concept

Generators are functions that can pause their execution and resume later, yielding values one at a time. They use the yield keyword instead of return.

Key generator concepts:

  • A function with yield returns a Generator object (implements Iterator).
  • The body doesn't execute until you call current(), next(), or iterate in foreach.
  • yield $value: Pause, give $value to the caller. Resume when next() is called.
  • yield $key => $value: Yield a key-value pair.
  • $input = yield $value: Bidirectional — caller can SEND a value back via $gen->send($data).
  • return $value: Sets the generator's return value (accessible via $gen->getReturn() after exhaustion).
  • yield from $iterable: Delegate to another generator/iterable.

Why generators:

  • Memory efficiency: Generate values on demand instead of building a full array.
  • A range(1, 1_000_000) array uses ~32MB. A generator that yields 1 to 1M uses ~1KB.
  • Lazy evaluation: No computation until the value is needed.
  • Infinite sequences: while(true) { yield nextValue(); } — works because values are pulled one at a time.

Coroutines: Generators with bidirectional communication (yield + send()). Used for cooperative multitasking. The caller controls when the generator runs. Basis of async frameworks like ReactPHP before native fibers.

Fibers (PHP 8.1+): Full coroutines with their own call stack. More powerful than generators for async work. Can be suspended/resumed from anywhere in the call stack (generators can only yield at the top level of the generator function).

Interview Q&A

Q: What does yield return to the generator? A: Whatever the caller passes to $gen->send($value). The first call to current() executes until the first yield$data = yield $value gets the SENT value, not the yielded value. Without send(), yield returns null.

Q: What's the difference between a Generator and an Iterator? A: Both implement Traversable and can be used in foreach. A Generator is a simpler way to write an Iterator — instead of a class with 5 methods (current, key, next, rewind, valid), you write a function with yield. The trade-off: a Generator can only be rewound to the start if you re-call the function (can't rewind() after starting).

Q: When would you use yield from? A: To compose generators. yield from $innerGenerator delegates iteration to another generator, flattening it into the outer generator's sequence. Return values propagate: $result = yield from inner() captures inner()'s return value.

Code Example

php
<?php
// Basic generator — memory-efficient range
function lazyRange(int $start, int $end, int $step = 1): \Generator
{
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i; // pause here, give $i to caller, resume on next()
    }
}

foreach (lazyRange(1, 1_000_000) as $n) {
    if ($n > 5) break;
    echo $n . ' '; // 1 2 3 4 5
} // Only 6 iterations ever ran — rest never computed

// Key-value generator
function indexedSquares(int $n): \Generator
{
    for ($i = 1; $i <= $n; $i++) {
        yield $i => $i * $i; // $key => $value
    }
}
foreach (indexedSquares(5) as $num => $square) {
    echo "{$num}² = {$square}\n"; // 1² = 1, 2² = 4, ...
}

// Bidirectional coroutine — generator as a service
function logger(): \Generator
{
    $messages = [];
    while (true) {
        $message = yield count($messages); // yield count, receive new message
        if ($message === null) break;
        $messages[] = $message;
    }
    return $messages; // accessible via getReturn()
}

$gen = logger();
$gen->current();          // initialize: execute until first yield → 0
$gen->send('First log');  // sends 'First log', resumes, yields count (1)
$gen->send('Second log'); // yields count (2)
$gen->send(null);         // terminates the loop
$all = $gen->getReturn(); // ['First log', 'Second log']

// yield from — compose generators
function flatten(array $items): \Generator
{
    foreach ($items as $item) {
        if (is_array($item)) yield from flatten($item); // recurse
        else                  yield $item;
    }
}
$flat = iterator_to_array(flatten([1, [2, 3], [4, [5, 6]]]));
// [1, 2, 3, 4, 5, 6]

// Eloquent cursor() returns a Generator — one model in memory at a time
// foreach (User::where('active', true)->cursor() as $user) {
//     processUser($user); // only 1 User hydrated at once
// }