Generators and coroutines — interview Q&A deep dive
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
yieldreturns aGeneratorobject (implementsIterator). - The body doesn't execute until you call
current(),next(), or iterate inforeach. yield $value: Pause, give$valueto the caller. Resume whennext()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
// 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
// }