Groovy 3.0 chunked method forces eager evaluation of lazy iterables
0 reputation · 04 Feb 2021, 07:50 UTC
0 reputation · 04 Feb 2021, 07:50 UTC
Goal: Enable pagination of large lazy iterables without loading the entire source into memory.
Constraint: The built‑in chunked method added in Groovy 3.0 eagerly realizes the whole iterable before creating chunks, which can cause out‑of‑memory errors for ranges or custom lazy sources.
Uncertainty: Whether the Groovy core library should provide a lazy variant (e.g., chunkedLazy) that yields chunks on demand, or whether developers must rely on custom implementations.
In Groovy 3.0 the chunked(int size) method is implemented by first converting the source iterable to a List (via toList()) and then partitioning that list. For a lazy source such as a large Range or a custom Iterable that generates values on demand, this conversion forces the entire sequence into heap memory before any chunk is produced. The result is an OutOfMemoryError when the source size exceeds available memory.
Adding a chunkedLazy method would give users a zero‑copy, stream‑like API and keep memory usage proportional to the chunk size. The trade‑offs are:
Iterable that lazily pulls from the source iterator, which is straightforward but requires careful handling of close() for resources like database cursors.chunked(). For very large sources the memory savings outweigh the overhead.Until such a method exists, developers must implement lazy chunking themselves.
The only guaranteed way to keep the source lazy is to consume its iterator directly and yield fixed‑size batches. The following generator‑style method works on any Iterable and never materialises more than one chunk:
def lazyChunk(Iterable source, int size) {
def iterator = source.iterator()
while (iterator.hasNext()) {
def chunk = []
int count = 0
while (iterator.hasNext() && count < size) {
chunk << iterator.next()
count++
}
yield chunk
}
}
Because the method uses yield, it must be declared with the @groovy.transform.Generator annotation (or be used inside a closure that is coerced to a Generator). The returned object implements Iterable and can be consumed in a for loop or passed to other lazy operators.
(1..10_000_000).chunked(100) and monitor heap with VisualVM – you will see a spike proportional to the range size.lazyChunk(1..10_000_000, 100) and verify that heap stays flat regardless of the range length.ResultSet or similar resource, ensure the iterator wrapper closes the resource when iteration finishes (e.g., by implementing AutoCloseable in the generator).One missing detail that could change the recommendation: What type of lazy source are you paging (plain Range, database cursor, custom Iterable)? The resource‑cleanup strategy differs for each.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.