Reading — step 1 of 7
Learn
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 timedef x = expensive— runs every accesslazy 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
Iteratorfor 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 case | Choose |
|---|---|
| Defer expensive single value | lazy val |
| Pipeline of operations on collection | .view (or eager if small) |
| Infinite sequence (Fibonacci, primes) | LazyList |
| One-shot streaming traversal | Iterator |
| Async deferred computation | Future (next lesson) |
Common mistakes
lazy valin performance-critical code — the synchronization on first access has overhead. For uncontended cases, preferval.- Treating
viewlike a regular collection — calls like.sizeon 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 effects —
lazy 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…