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 (List, Vector)

Yes

Yes (it is the data)

Data you keep and reuse

View (xs.view)

Yes

No: recomputed on each traversal

A pipeline of transformations, forced once

Iterator

No: single use

No

One pass over a large or external source

LazyList

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)

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)

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)

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

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

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)

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)

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

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, reverse and forall on LazyList.from(1) run forever; bound it first with take or takeWhile. Likewise LazyList.from(1).filter(_ < 0).headOption searches forever, because the filter never finds a match.

  • Holding the head holds the memory. A LazyList remembers 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 a val that outlives the traversal, or use an Iterator when 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 Stream for infinite sequences. In 2.13 and Scala 3 use LazyList, whose head is lazy too; Stream is deprecated.

  • Older material calls .force on views and relies on view returning a view of the same collection type (List view). In 2.13 views no longer know their underlying collection; .force is deprecated and always gives an IndexedSeq. Convert explicitly with toList or to(...).

  • Books written before 2.13 describe mapValues and filterKeys as lazy views whose results look like maps. In 2.13 both are deprecated on Map; use .view.mapValues(...).

  • The 2.12 .par parallel collections are a separate library now (see Parallel Collections & Concurrency Libraries).