Skip to content
Lazy Evaluation: lazy val, view, LazyList
step 1/7

Reading — step 1 of 7

Learn

~3 min readCollections in Depth

Scala has several ways to defer computation until needed. The Programming in Scala chapter on iteration touches on each.

lazy val — defer until first access

lazy val expensive = {
    println("computing...")
    Thread.sleep(100)
    42
}

println("about to access")
println(expensive)            // "computing..." then 42
println(expensive)            // 42 — already cached

The initializer runs ONCE on first access. Subsequent accesses return the cached result. Thread-safe by default (uses double-checked locking).

Use cases:

  • Configuration values that may not always be needed
  • Cyclic dependencies during initialization
  • Memoization of single-value computation

Differences from val and def:

  • val x = expensive — eager; runs at definition time
  • def x = expensive — runs every access
  • lazy val x = expensive — runs on first access, cached forever

view — lazy collections

For pipelines on large collections, .view defers operations until materialization:

val nums = (1 to 1_000_000).toList

// Eager — builds intermediate List for each step
val result1 = nums.map(_ * 2).filter(_ > 100).take(10)
// 3 separate lists allocated

// Lazy — fused into one pass, stops after 10
val result2 = nums.view.map(_ * 2).filter(_ > 100).take(10).toList
// No intermediate lists; map/filter only run for ~50 elements

The view doesn't compute anything. The terminal toList (or foreach, sum, etc.) drives evaluation, consuming only what's needed.

For short pipelines or small collections, the eager version is faster (no view overhead). For long pipelines or large collections with early termination (take, find), views win.

LazyList (Scala 2.13+) — infinite lazy sequences

Replaces the older Stream:

val naturals: LazyList[Int] = LazyList.from(1)

naturals.take(5).toList                            // List(1, 2, 3, 4, 5)
naturals.filter(_ % 7 == 0).take(3).toList         // List(7, 14, 21)

// Infinite Fibonacci
lazy val fibs: LazyList[BigInt] = BigInt(0) #:: BigInt(1) #:: fibs.zip(fibs.tail).map { case (a, b) => a + b }

fibs.take(10).toList
// List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)

#:: is the lazy cons operator — the tail isn't evaluated until needed.

Caveats:

  • Each evaluated element is CACHED — long retention can leak memory
  • Operations on a LazyList are themselves lazy
  • Use Iterator for one-shot lazy traversal without caching

Iterator — single-pass lazy traversal

val iter = (1 to 1_000_000).iterator
    .map(_ * 2)
    .filter(_ > 1000)

iter.take(5).foreach(println)   // computes only what's needed
// after this, iter is consumed

Iterator is one-shot — once consumed, it's done. No caching, no memory growth. Idiomatic for streaming through huge data.

When to use which

Use caseChoose
Defer expensive single valuelazy val
Pipeline of operations on collection.view (or eager if small)
Infinite sequence (Fibonacci, primes)LazyList
One-shot streaming traversalIterator
Async deferred computationFuture (next lesson)

Common mistakes

  • lazy val in performance-critical code — the synchronization on first access has overhead. For uncontended cases, prefer val.
  • Treating view like a regular collection — calls like .size on a view don't materialize; they walk the source.
  • LazyList memory leaks — keeping a reference to the head retains all evaluated elements. Drop the head reference after iterating.
  • Iterator reuse — once consumed, an iterator is empty. To iterate twice, build it fresh or use a re-iterable source.
  • Lazy side effectslazy val x = { sideEffect(); 1 } runs the side effect only on first access. Surprising for code expecting eager init.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…