Views, Iterators & LazyList
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). The collections library is shared by all Scala 3 versions and by Scala 2.13, so everything below applies unchanged to the 3.9 LTS and 3.3 LTS lines.
By default Scala collections are strict: an operation such as map computes every element immediately. Three tools
make a computation non-strict (elements computed only when needed): views, iterators and LazyList. They answer
different questions, so choosing between them is the point of this page. The strict operations are covered in
Collection Operations; the laziness of lazy val and by-name
parameters in Laziness. The examples use the trailhead scenario of this
section, a toolkit that plans hikes on a network of mountain trails.
| Tool | Re-traversable | Caches results | Typical use |
|---|---|---|---|
Strict collection ( |
Yes |
Yes (it is the data) |
Data you keep and reuse |
View ( |
Yes |
No: recomputed on each traversal |
A pipeline of transformations, forced once |
|
No: single use |
No |
One pass over a large or external source |
|
Yes |
Yes (memoised) |
Possibly infinite sequences, evaluated on demand and kept |
Strict versus non-strict
The same chain of operations on a strict collection and on its view differs in what it computes. The function below prints a line each time it runs.
//> using scala 3.10.0
def noisy(n: Int): Int = { println(s" computing $n"); n * n }
@main def c1(): Unit =
val xs = List(1, 2, 3, 4)
println("strict:")
println(xs.map(noisy).take(2))
println("view:")
val v = xs.view.map(noisy).take(2)
println(v)
println("forcing once:")
println(v.toList)
println("forcing again:")
println(v.toList)
strict:
computing 1
computing 2
computing 3
computing 4
List(1, 4)
view:
SeqView(<not computed>)
forcing once:
computing 1
computing 2
List(1, 4)
forcing again:
computing 1
computing 2
List(1, 4)
Source: Collections: Views
The strict map computed four squares and take discarded two. The view computed nothing when it was built (printing it
shows SeqView(<not computed>)), then computed only the two elements that take(2) needs, and did it again the second
time it was traversed. Views never cache.
Views
xs.view returns a view of the collection: an object with the usual transformation methods (map, filter, take,
slice, zip, …) that record the transformation instead of performing it. A transformation on a view returns another
view. The elements are computed when the view is forced by converting it to a strict collection (toList,
to(Vector), toSeq) or by a method that must look at the elements (foreach, sum, headOption, mkString).
//> using scala 3.10.0
@main def c1b(): Unit =
val xs = List(1, 2, 3, 4)
println(xs.view.map(_ + 1).to(Vector))
println(xs.view.filter(_ % 2 == 0).map(_ * 10).headOption)
println((1 to 1_000_000).view.map(_ * 2).filter(_ % 3 == 0).take(3).toList)
println(xs.view.slice(1, 3).toList)
println(xs.view.zipWithIndex.toList)
println(Map("a" -> 1, "b" -> 2).view.mapValues(_ + 1).toMap)
Vector(2, 3, 4, 5)
Some(20)
List(6, 12, 18)
List(2, 3)
List((1,0), (2,1), (3,2), (4,3))
Map(a -> 2, b -> 3)
Source: Collections: Views
The third line illustrates the main benefit. The strict version would build a list of a million doubled integers, then a
filtered list, before taking three; the view examines only the first nine integers (6, 12, 18 are the doubles of 3,
6 and 9) and allocates no intermediate collection. headOption on a filtered view stops at the first match. The last
line shows the usual way to get a lazily-mapped Map: .view.mapValues(...) and then toMap when you need a real map.
A view is also the right tool when you do not want to produce a large intermediate copy: xs.view.slice(a, b) is a window
on the original data rather than a copy.
Forcing a view
Convert a view back to a strict collection with an explicit conversion: toList, toVector, toSeq or
to(Vector) for any collection type. The old force method still exists but is deprecated since Scala 2.13.0
(Views no longer know about their underlying collection type; .force always returns an IndexedSeq), and Scala CLI prints a
deprecation warning when you use it.
Views see later changes to a mutable collection
A view reads the underlying collection when it is forced, so on a mutable collection the result depends on when you force it:
//> using scala 3.10.0
import scala.collection.mutable.ArrayBuffer
@main def c1c(): Unit =
val buf = ArrayBuffer(1, 2, 3)
val vb = buf.view.map(_ * 10)
buf += 4
println(vb.toList)
val snapshot = buf.view.map(_ * 10).toList
buf += 5
println(snapshot)
List(10, 20, 30, 40)
List(10, 20, 30, 40)
Source: Collections: Views
The first view was created while the buffer held three elements but forced after the fourth was added, so it shows four.
The strict snapshot is unaffected by the later += 5. Treat a view over a mutable collection as a window onto its current
contents, and force it promptly.
When a view helps and when it hurts
A view pays off when a chain of operations ends by discarding most of the data (take, headOption, find, exists),
when the source is large and the intermediate collections would be wasted, or when only part of the result is read. It
hurts when the same view is traversed several times (everything is recomputed each time), when the function passed to it is
expensive, or when the code relies on side effects that now run at the time of forcing. If you need the results more than
once, force the view into a strict collection once.
Iterators
An Iterator is a way to step through elements one at a time with hasNext and next(). It has the same transformation
methods as a collection, and they are lazy: map, filter, take and the others return a new iterator that does the work
as elements are pulled. Unlike a view, an iterator is a single-use object: it advances as it is consumed and cannot be
rewound.
//> using scala 3.10.0
@main def c2(): Unit =
val it = Iterator(10, 20, 30, 40)
println(it.hasNext)
println(it.next())
println(it.map(_ + 1))
val it2 = Iterator(1, 2, 3)
val doubled = it2.map { x => println(s" doubling $x"); x * 2 }
println("nothing printed yet")
println(doubled.toList)
println(doubled.toList)
println(doubled.hasNext)
true
10
<iterator>
nothing printed yet
doubling 1
doubling 2
doubling 3
List(2, 4, 6)
List()
false
Source: Collections: Iterators
Printing an iterator shows only <iterator>. The map ran when toList pulled the elements, and the second toList returns
an empty list because the iterator was already exhausted. This is the most common iterator bug: traversing the same iterator
twice, or calling size or length (which consume it) and then using it again.
//> using scala 3.10.0
@main def c7(): Unit =
val it = Iterator(1, 2, 3).filter(_ > 0)
println(it.size)
println(it.hasNext)
3
false
Source: Collections: Iterators
Calling size consumed the iterator. (When an iterator knows its size in advance, for instance a freshly created
Iterator(1, 2, 3) or one mapped from it, size reads that knowledge without consuming it, so the same call can behave
differently depending on the pipeline. Never rely on that; assume that any method that gives you a result has used the
iterator up.)
The rule of the library documentation is: once you call a method on an iterator, do not use that iterator again, except
for next and hasNext. The next example shows take consuming the underlying iterator, and duplicate as the way to
get two independent iterators:
//> using scala 3.10.0
@main def c2b(): Unit =
val it3 = Iterator(1, 2, 3, 4, 5)
val firstTwo = it3.take(2).toList
println(firstTwo)
println(it3.toList)
val it4 = Iterator(1, 2, 3, 4, 5)
val (a, b) = it4.duplicate
println(a.toList)
println(b.map(_ * 10).toList)
List(1, 2)
List(3, 4, 5)
List(1, 2, 3, 4, 5)
List(10, 20, 30, 40, 50)
Source: Collections: Iterators
After it3.take(2) was read the original iterator still held the remaining elements, but that is a behavior of the
implementation and not a guarantee, so the library discourages it; duplicate buffers elements for the lagging copy, so
it can use memory if one side runs far ahead of the other.
Creating iterators
Iterator has constructors for the common sources, including infinite ones. They are lazy, so an infinite iterator is safe
as long as you bound it before consuming it.
//> using scala 3.10.0
@main def c2c(): Unit =
println(Iterator.from(5).take(3).toList)
println(Iterator.iterate(1)(_ * 2).take(6).toList)
println(Iterator.continually("x").take(3).mkString)
println(Iterator.range(0, 10, 3).toList)
println(Iterator(1, 2, 3, 4, 5, 6, 7).grouped(3).toList)
println(Iterator(1, 2, 3).zipWithIndex.toList)
println(List(1, 2, 3).iterator.sum)
println(Iterator(1, 2, 3).sameElements(List(1, 2, 3)))
val lines = scala.io.Source.fromString("one\ntwo\nthree").getLines()
println(lines.map(_.toUpperCase).toList)
List(5, 6, 7)
List(1, 2, 4, 8, 16, 32)
xxx
List(0, 3, 6, 9)
List(ArraySeq(1, 2, 3), ArraySeq(4, 5, 6), ArraySeq(7))
List((1,0), (2,1), (3,2))
6
true
List(ONE, TWO, THREE)
Source: Scala API: Iterator object
Every collection has an iterator method, Source.getLines() returns an iterator over the lines of a file or string, and
Iterator.from, iterate and continually create unbounded sequences. Iterators are the right tool for a single pass over
data too large to hold in memory, or produced by something external such as a file or a socket; for files and processes see
Files & Processes with OS-Lib.
BufferedIterator
buffered turns an iterator into a BufferedIterator, which adds head: a look at the next element without consuming it.
That is what a parser needs to decide, for example, where a run of similar elements ends.
//> using scala 3.10.0
@main def c2d(): Unit =
val b5 = Iterator(1, 2, 3, 10, 11).buffered
while b5.hasNext && b5.head < 5 do println(s"small: ${b5.next()}")
println(b5.head)
println(b5.toList)
small: 1
small: 2
small: 3
10
List(10, 11)
The loop stopped at 10 without consuming it, so the remaining elements are still available. Without buffered, you would
have to read 10 with next() and then find some other place to put it back.
LazyList
A LazyList is an immutable linked list whose elements are computed on demand and then remembered. It is the right choice
when a sequence is possibly infinite, when the elements depend on earlier ones, or when you want to keep computed results.
The #:: operator builds one, with the tail evaluated lazily. A freshly built list shows <not computed>, and a list shows
only the elements that have been forced so far.
//> using scala 3.10.0
@main def c3(): Unit =
val ll = 1 #:: 2 #:: 3 #:: LazyList.empty
println(ll)
println(ll.toList)
println(ll)
def noisy(n: Int): Int = { println(s" computing $n"); n * n }
val sq = LazyList.from(1).map(noisy)
println(sq)
println(sq.take(3).toList)
println(sq.take(3).toList)
println(sq)
println(sq.head)
LazyList(<not computed>)
List(1, 2, 3)
LazyList(1, 2, 3)
LazyList(<not computed>)
computing 1
computing 2
computing 3
List(1, 4, 9)
List(1, 4, 9)
LazyList(1, 4, 9, <not computed>)
1
The sq list is infinite, and the infinity is safe because only what is asked for is computed. The squares of 1, 2 and
3 were computed once, by the first take(3).toList; the second take(3).toList reused them (nothing is printed), which is
the difference from a view. After the evaluation the list shows its three computed elements followed by <not computed>.
Self-referential definitions
Because the tail is lazy, a LazyList can be defined in terms of itself. The classic example is the Fibonacci sequence:
//> using scala 3.10.0
@main def c3b(): Unit =
lazy val fibs: LazyList[BigInt] =
BigInt(0) #:: BigInt(1) #:: fibs.zip(fibs.tail).map(_ + _)
println(fibs.take(12).toList)
println(fibs(100))
List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
354224848179261915075
Source: Collections: LazyLists
Each element is the sum of the two before it, and the list memoises, so fibs(100) computes each number once. The lazy val
is required for a self-reference in a local definition (see Laziness).
Factory methods
LazyList has the same family of constructors as the other collections plus the infinite ones:
//> using scala 3.10.0
@main def c3c(): Unit =
println(LazyList.iterate(1)(_ * 3).takeWhile(_ < 500).toList)
println(LazyList.continually(7).take(3).toList)
println(LazyList.unfold(10)(n => if n <= 0 then None else Some((n, n - 3))).toList)
println(LazyList.tabulate(5)(i => i * i).toList)
println(LazyList.from(1).filter(_ % 7 == 0).drop(2).head)
println(LazyList.range(1, 6))
println(LazyList(1, 2, 3).lazyAppendedAll(LazyList(4, 5)).toList)
println(LazyList(1, 2) #::: LazyList(3))
List(1, 3, 9, 27, 81, 243)
List(7, 7, 7)
List(10, 7, 4, 1)
List(0, 1, 4, 9, 16)
21
LazyList(<not computed>)
List(1, 2, 3, 4, 5)
LazyList(<not computed>)
unfold builds a sequence from a seed: the function returns Someelement, nextSeed to continue and None to stop.
iterate repeats a function from a start value, and continually repeats a value. #::: concatenates two lazy lists
lazily, and nothing is evaluated until the result is read (the last line prints <not computed>).
Pitfalls of LazyList
-
Operations that need every element do not terminate on an infinite list.
size,last,toList,sum,reverseandforallonLazyList.from(1)run forever; bound it first withtakeortakeWhile. LikewiseLazyList.from(1).filter(_ < 0).headOptionsearches forever, because the filter never finds a match. -
Holding the head holds the memory. A
LazyListremembers every element that has been evaluated, so keeping a reference to the start of a long computed list keeps it all alive. Pass the list along without storing it in avalthat outlives the traversal, or use anIteratorwhen you do not need the cache. -
Side effects in the generating function run once per element, at the time of first access, not at construction.
LazyList replaces Stream
LazyList replaced Stream in Scala 2.13. Stream was evaluated lazily only in its tail, so its head was always
computed; LazyList is lazy in the head as well, which removes a family of surprises. Stream is still present as a
deprecated alias, and compiling code that uses it prints a warning:
-- Deprecation Warning: c5.scala:3:10
3 | val s = Stream.from(1)
| ^^^^^^
|value Stream in package scala is deprecated since 2.13.0: Use LazyList instead of Stream
Replace Stream with LazyList; the operations and the #:: operator are the same.
Creating collections from scratch
Every collection companion has the same set of factory methods, so the constructors above are the general pattern:
//> using scala 3.10.0
@main def c4(): Unit =
println(List.fill(3)("x"))
println(List.fill(2, 3)(0))
println(Vector.tabulate(5)(i => i * i))
println(List.tabulate(2, 3)((r, c) => r * 10 + c))
println(List.iterate(1, 6)(_ * 2))
println(List.range(0, 10, 3))
println(List.unfold(1)(n => if n > 40 then None else Some((n, n * 3))))
println(List.concat(List(1), Vector(2), Set(3)))
println(Set.from(List(1, 1, 2)))
println(Map.from(List("a" -> 1, "b" -> 2)))
println((1 to 10 by 3).toList)
val b = Vector.newBuilder[Int]
b += 1
b ++= List(2, 3)
println(b.result())
println(List.from(Iterator(1, 2, 3)))
println(Iterator.unfold("a")(s => if s.length > 3 then None else Some((s, s + "a"))).toList)
List(x, x, x)
List(List(0, 0, 0), List(0, 0, 0))
Vector(0, 1, 4, 9, 16)
List(List(0, 1, 2), List(10, 11, 12))
List(1, 2, 4, 8, 16, 32)
List(0, 3, 6, 9)
List(1, 3, 9, 27)
List(1, 2, 3)
Set(1, 2)
Map(a -> 1, b -> 2)
List(1, 4, 7, 10)
Vector(1, 2, 3)
List(1, 2, 3)
List(a, aa, aaa)
fill(n)(elem) repeats an element (the element expression is evaluated for each position, so List.fill(3)(random()) gives
three different values), tabulate(n)(f) computes an element from its index, iterate(start, n)(f) repeats a function, and
range produces numeric sequences. The two-argument forms fill(2, 3) and tabulate(2, 3) build nested collections.
unfold is the general constructor from a seed. from converts any IterableOnce, and newBuilder gives a mutable
builder for code that accumulates elements in a loop and wants an immutable result at the end (the code of the library’s
own methods uses builders).
Choosing between them
-
Data you will read many times: a strict collection.
-
A chain of transformations on a strict collection that you will traverse once, especially if most of the result is discarded: a view.
-
A single pass over a large or external source, or where you do not need to keep the elements: an
Iterator. -
An infinite or incrementally computed sequence whose computed part you want to reuse: a
LazyList. -
Concurrent or asynchronous sequences are a different tool; see Futures & Promises.
Java’s Stream is lazy like an iterator and is single use (and Stream.iterate with a stop predicate plays the role of unfold); it is covered in
Streams & Collectors. Kotlin’s Sequence is lazy and
re-traversable without caching, which makes it the counterpart of a Scala view or iterator-based pipeline; see
Collections & Sequences. The Java collections that
hold the data are in Collections Framework. Neither has a
direct counterpart of the memoising LazyList.
Where the books are outdated
-
Scala 2.12 and earlier books use
Streamfor infinite sequences. In 2.13 and Scala 3 useLazyList, whose head is lazy too;Streamis deprecated. -
Older material calls
.forceon views and relies onviewreturning a view of the same collection type (Listview). In 2.13 views no longer know their underlying collection;.forceis deprecated and always gives anIndexedSeq. Convert explicitly withtoListorto(...). -
Books written before 2.13 describe
mapValuesandfilterKeysas lazy views whose results look like maps. In 2.13 both are deprecated onMap; use.view.mapValues(...). -
The 2.12
.parparallel collections are a separate library now (see Parallel Collections & Concurrency Libraries).