LazyList¶
A lazy list that remembers what it computed. Nothing is computed before it is read, not even its first element or whether it is empty: each element is computed when it is first reached, and then kept. It can be infinite.
Unlike a java.util.stream.Stream, which is a one-shot pipeline, a LazyList is a collection: it can be read
many times, and each read after the first reuses what was computed.
When to choose it¶
For a sequence computed on demand: an infinite series, a sequence whose elements are expensive and only partly read, or a sequence defined in terms of itself.
var naturals = LazyList.from(1); // LazyList<Integer>
var squares = naturals.map(n -> n * n).filter(n -> n % 2 == 1).take(4).toVector();
// Vector(1, 9, 25, 49)
var fibonacci = LazyList.of(0L, 1L).appendSelf(self -> self.zipWith(self.tail(), Long::sum));
var firstTen = fibonacci.take(10).toVector(); // Vector<Long>
// Vector(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
LazyList.iterate(seed, f), LazyList.continually(supplier) and LazyList.ofAll are other ways to create one.
Nothing is computed before it is read¶
Building a LazyList and calling map, filter or appendAll on it compute nothing. Reading an element computes
the elements it needs, once, even when several threads read it.
var seen = new java.util.ArrayList<Integer>();
var squares = LazyList.from(1).tap(seen::add).map(n -> n * n); // LazyList<Integer>
// seen is empty: nothing is computed yet
var third = squares.get(2); // 9, and seen is [1, 2, 3]
LazyList.cons(head, () -> tail) takes its first element as a value. LazyList.defer(() -> ...) computes the whole
list, first element included, when it is first read.
Costs¶
lazy means the call computes nothing: each element of the result is computed when it is first read. The note
says what reading the first element computes: filter and the calls like it the elements up to the first one they
keep, drop, slice and dropRight the elements they skip or hold back, rotateLeft or takeRight the whole
list. reverse, sorted and scanRight compute the whole list when they are called.
| Operation | Cost | Note |
|---|---|---|
head |
O(1) | O(1) once this LazyList is computed; otherwise the call computes it first, which is where a lazy operation does its work (such as the search of filter), and keeps the result. |
tail |
O(1) | O(1) once this LazyList is computed: the tail is returned without being computed. If this LazyList is not computed yet, the call computes it first, as head does. On a LazyList built by append or appendAll, computing the first appended element may put the p appended parts in order, O(p) once for the whole walk. |
last |
O(n) | O(n); the whole LazyList is computed. |
init |
lazy | lazy; the first element is computed now, to know that this LazyList is not empty, and the result reads one element ahead of what it returns. |
get |
O(i) | O(i); the first i + 1 elements are computed. |
update |
lazy | lazy; nothing is computed now. The result copies the elements before index i as it reaches them and shares those after it; an index past the end throws when the result reaches it. |
prepend |
O(1) | O(1); nothing is computed. |
append |
O(1) | O(1); nothing is computed now, and the element comes after the last one of this LazyList. Appending in a loop stays O(1) per call, and reading the result back costs O(1) per element, however many calls built it. |
prependAll |
O(1) | O(1); nothing is computed now: the given elements are read when the result reaches them, and this LazyList is shared, not read. Calling prependAll in a loop stays O(1) per call, and reading the result back costs O(1) per element, however many calls built it. |
appendAll |
O(1) | O(1); nothing is computed now, neither of this LazyList nor of the given elements, which are read when the result reaches them, so an infinite argument is fine. They are read once, into a LazyList that every result built from this one shares. Calling appendAll or append in a loop stays O(1) per call, and reading the result back costs O(1) per element, however many calls built it. |
insert |
lazy | lazy; nothing is computed now. The result copies the elements before index i as it reaches them, and shares the rest. |
removeAt |
lazy | lazy; nothing is computed now. The result copies the elements before index i as it reaches them, and shares the rest. |
take |
lazy | lazy; nothing is computed now, each element when the result reaches it. |
drop |
lazy | lazy; nothing is computed now. Reading the first element computes the k dropped elements and the first one kept, O(k); the rest are computed when the result reaches them. A drop of a drop not read yet is one drop of the sum, so a chain of drops is read at the stack depth of one. |
slice |
lazy | lazy; nothing is computed now. Reading the first element computes the first i + 1 elements, O(i), and the rest up to index j are computed when the result reaches them, so it works on an infinite LazyList. |
splitAt(Predicate<? super T>) |
lazy | lazy; nothing is computed now, and each side computes its elements when it is read: the suffix starts at the first match. |
splitAt(int) |
lazy | lazy; nothing is computed now, and each side computes its elements when it is read. |
reverse |
O(n) | O(n); the whole LazyList is computed now. |
sorted |
O(n log n) | O(n log n) comparisons; the whole LazyList is computed now. |
size |
O(n) | O(n): the whole LazyList is computed and walked at each call, so it never returns on an infinite LazyList. |
contains |
O(n) | O(n): the elements are compared one by one until an equal one is found. The sets and the maps override it with a lookup. |
indexOf |
O(n) | O(n); the elements are computed until the element is found. |
zip |
lazy | lazy; nothing is computed now, and reading every pair costs O(min(n, m)). |
sliding(int) |
lazy | lazy; nothing is computed now, and reading every window costs O(n * size). Moving to the next window computes the elements of the current one and one more, so an infinite LazyList can be windowed. |
sliding(int, int) |
lazy | lazy; nothing is computed now, and reading every window costs O(n + (n / step) * size). Moving to the next window computes the elements up to max(size, step) positions after the start of the current one, so an infinite LazyList can be windowed. |
grouped |
lazy | lazy; nothing is computed now, and reading every block costs O(n). Moving to the next block computes the elements of the current one and one more, so an infinite LazyList can be grouped. |
distinct |
lazy | lazy; nothing is computed now. Moving to the next element skips and hashes the repeated ones before it, which never ends on an infinite LazyList with no further new element. |
Every method: complexity page.
Sharp edges¶
- Operations that need the whole sequence compute it and never return on an infinite
LazyList:size,last,reverse,sorted,max,min,foldRight,groupBy,lastIndexOfSlice(that)andhashCode, and anything else that reads every element, such asfoldLeft,mkStringortoVector. equalsstops at the first difference or at the end of the shorter side, so an infiniteLazyListcompared with a finiteList,Vector,QueueorLazyListreturns. Two infiniteLazyLists with the same elements never do.filterand the calls like it (reject,retainAll,removeAll,collect,flatMap,distinct) compute elements until they find one to keep, each time the result is read further. On an infiniteLazyListwith nothing more to keep, reading the next element never returns.isEmpty()reads: it runs that search too.partitionandpartitionMapreturn at once, but reading a side that stays empty on an infiniteLazyListnever returns.- An out-of-range index given to
insert,insertAll,removeAtorupdatethrows when the result is read that far, not when the method is called (unless theLazyListis already known to be empty). A negative index throws at once. LazyList.cons(head, supplier)calls the supplier when the tail is read, not whentail()returns it: a supplier that returns null fails there.- When computing an element throws, the
LazyListkeeps the exception in its place: reading that element again throws the same exception, and never skips to the next one. Only aVirtualMachineError, such as a stack overflow, lets a later read try again. - A chain of thousands of lazy operations (
map,filter,take,deferinsidedefer) built without reading anything is evaluated recursively on the first read, as in Scala, and can overflow the stack then. Reading as you go, or reading on a thread with a bigger stack, avoids it. A chain ofdrops, or a loop ofappendorappendAll, has no such limit. - A
LazyListkeeps every element it computed. Holding on to the start of a longLazyListwhile walking it keeps all of it in memory.