DEV Community

Recca Tsai
Recca Tsai

Posted on • Originally published at recca0120.github.io

Laravel LazyCollection Loses Lazy Evaluation with Generator

Originally published at recca0120.github.io

The Code I Thought Would Work

After reading the docs, I knew LazyCollection with Generator enables lazy loading. So I passed a generator directly in, expecting the loop to stop once it found $i > 5:

$this->assertEquals(6, LazyCollection::make($this->generator())->collapse()->first(function ($i) {
    return $i > 5;
}));
Enter fullscreen mode Exit fullscreen mode

It didn't. The loop ran through all 9 iterations before stopping.

Why

After digging into the LazyCollection source code, I figured it out. If the constructor receives something that isn't a Closure, it falls through to getArrayableItems(). Since Generator implements Traversable, it gets fully expanded by iterator_to_array() all at once:

protected function getArrayableItems($items)
{
    // ...
    } elseif ($items instanceof Traversable) {
        return iterator_to_array($items);
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The lazy loading effect is completely lost.

Eager vs Lazy Generator execution difference

The Correct Approach

Wrap it in a Closure so that LazyCollection receives "a function that produces a Generator" rather than an already-running Generator:

$this->assertEquals(6, LazyCollection::make(function () {
    return $this->generator();
})->collapse()->first(function ($i) {
    return $i > 5;
}));
Enter fullscreen mode Exit fullscreen mode

This way only the first iteration runs before stopping -- that's true lazy loading.

The distinction is whether you pass the Generator itself or a Closure that produces one.

Top comments (0)