Scala Lazy Evaluation
Explore Scala's lazy evaluation and how it defers computation until needed. Learn through examples and see how it compares to Python's generators.
Focus: explore scala's lazy evaluation
As a Python developer, you've probably felt the pain of eagerly building an entire list or range, only to use a fraction of it — wasting memory and CPU for no good reason. Python's generators and itertools offer a partial escape hatch, but they're often bolted on after the fact. Scala takes a different, more principled approach: laziness is a first-class citizen of the language and its standard library. In this lesson, you'll explore Scala's lazy evaluation — how it defers computation until the last possible moment, how it compares to Python's generators, and when you should (and shouldn't) use it.
The problem this lesson solves
Eager evaluation is the default in most programming languages: when you write an expression like val nums = List(1, 2, 3).map(_ * 2), Scala immediately computes and stores the entire transformed list. That's fine for small data, but what about a million elements? Or a potentially infinite stream (think: all prime numbers, or sensor readings)? Eager evaluation would either exhaust memory or crash the program. Python solves this with generators using yield, but it's often seen as an afterthought — you must deliberately choose generator expressions or write custom iterator classes. Scala's lazy evaluation is woven into the language itself, offering elegant, composable abstractions like lazy val, view, and Stream/LazyList. This lesson bridges that gap, showing you how to think and code lazily in Scala, and why it matters for performance and responsiveness in real-world JVM applications.
Core concept / mental model
Think of evaluation like cooking a meal. Eager evaluation is like pre-cooking every dish on the menu, even if your customers only order one item — wasteful. Lazy evaluation is like a chef who waits until an order arrives before starting to prepare the dish — each ingredient is processed only when needed. In programming, laziness means deferring computation until the result is actually required, and potentially never doing it at all if the result is never used.
Scala offers several tools for laziness, each with a slightly different mental model:
lazy val: A single-value lazy binding — computed at most once, on first access. Great for expensive initializations (e.g., loading a config file or database connection).view: A lazy transformation on a collection — operations likemap,filterare applied on-demand as you iterate. This avoids building intermediate collections.LazyList(formerlyStream): A lazily evaluated linked list that can represent infinite sequences as long as you only consume what you need.
Think of LazyList as Python's itertools.count() on steroids: it's a sequence that computes its next element only when you ask for it, but it also remembers past elements if you keep a reference (memoization).
How it works step by step
To master Scala's lazy evaluation, follow this mental progression:
- Identify eager hotspots — Look for chains of
map,filter,flatMap, etc., that build intermediate collections. If the collection is large or infinite, that's a smell. - Choose your lazy tool — For a single expensive value, use
lazy val. For transformations on finite collections, use.view. For infinite or streaming sequences, useLazyList. - Convert or construct — Turn an existing collection into a view with
.view, or build aLazyListfrom a function:LazyList.from(1)orLazyList.iterate(start)(f). - Transform and consume — Apply transformations like
mapandfilteron the lazy structure — they become lazy as well. Then consume withtake,foreach,toList, etc. Only the consumed elements are evaluated. - Exploit short-circuiting — Use
take(n)to grab a finite number of elements from an infinite sequence. Onlynelements are computed.
Key differences from Python
| Python | Scala | Why it matters |
|---|---|---|
generator / yield |
LazyList |
Both defer computation, but Scala's is stricter and composable |
itertools.islice |
.take(n) |
Similar, but Scala's is a built-in method |
map, filter return lists |
.view makes them lazy |
Scala lets you make transform chains lazy without changing syntax |
| No built-in memoization | LazyList memoizes |
Avoids re-computation on repeated access |
But beware: laziness is not a silver bullet. It adds overhead (each access is a function call), and it can delay errors until use time rather than definition time — which can surprise you.
Hands-on walkthrough
Let's see this in action with runnable examples.
Example 1: lazy val for expensive initialization
object LazyValExample {
// Simulate an expensive computation
def expensive: String = {
println("Computing...")
"result"
}
def main(args: Array[String]): Unit = {
lazy val cached = expensive
println("Before access")
println(cached) // computes here
println(cached) // uses cached value
}
}
Expected output:
Before access
Computing...
result
result
Notice Computing... prints only once — the lazy val is evaluated on first use and then memoized.
Example 2: view to avoid intermediate collections
object ViewExample {
def main(args: Array[String]): Unit = {
val nums = (1 to 1000000).toList
// Eager: builds two full intermediate lists
val eager = nums.map(_ * 2).filter(_ > 10).take(5)
// Lazy: only computes what's needed
val lazyResult = nums.view.map(_ * 2).filter(_ > 10).take(5).toList
println(eager)
println(lazyResult)
}
}
Expected output:
List(12, 14, 16, 18, 20)
List(12, 14, 16, 18, 20)
Here, .view defers the map and filter until take(5) is called, avoiding the creation of two million-element intermediate lists.
Example 3: LazyList for infinite sequences
object LazyListExample {
def main(args: Array[String]): Unit = {
// Infinite sequence of natural numbers
val naturals: LazyList[Int] = LazyList.from(1)
// Take first 5 even numbers
val evens = naturals.filter(_ % 2 == 0).take(5).toList
println(evens)
// Fibonacci as a lazy stream
lazy val fib: LazyList[Int] = 0 #:: 1 #:: fib.zip(fib.tail).map { case (a, b) => a + b }
println(fib.take(10).toList)
}
}
Expected output:
List(2, 4, 6, 8, 10)
List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
The #:: operator constructs a lazy stream, and the Fibonacci definition is elegantly self-referential — it would be impossible eagerly because it would recurse infinitely.
Compare options / when to choose what
Scala's laziness isn't a single tool — it's a spectrum. Here's when to reach for each:
| Use case | lazy val |
.view |
LazyList |
|---|---|---|---|
| Expensive single values (config, DB) | ✅ Best | ✗ | ✗ |
| Chained transforms on finite data | ✗ | ✅ Best | ✗ |
| Infinite sequences / streaming data | ✗ | ✗ | ✅ Best |
| Memory footprint reduction | ✅ Helps | ✅ Helps | ✅ Helps |
| Performance overhead | Low | Medium | Higher (memoization) |
Pro tip: If you only need a finite subset of an infinite sequence,
LazyList+takeis ideal. But if the sequence is finite and manageable, eager evaluation is often simpler and faster due to less overhead.
Variations to consider
Iterator: A mutable, one-pass alternative. It's lazy likeLazyListbut doesn't memoize, so you can't go back. Good for single-pass processing of large files.Stream(legacy): The old name forLazyListin Scala 2.12 and earlier. In modern Scala (2.13+), useLazyList. The API is essentially the same, butStreammay not be as optimized.- Custom lazy classes: For complex laziness, you can combine
lazy valand functions. Rarely needed, but powerful.
Troubleshooting & edge cases
Laziness can bite you in subtle ways. Here are common pitfalls:
1. StackOverflowError on long chains
If you build a deep chain of lazy operations (like a long LazyList), you might hit stack overflow because each #:: node adds a stack frame. Use .iterator or foreach to avoid deep recursion in recursive definitions.
2. Unintentional memoization
LazyList stores computed values. If you keep a reference to the head of a huge lazy list, memory usage grows as you consume. For single-pass use, Iterator is better.
3. Lazy values not initializing in the expected order
lazy val evaluates on first access, but if you access it from multiple threads, only one computes and others wait — but the order of initialization among multiple lazy vals can be surprising. Avoid circular dependencies between lazy vals.
4. Side effects in lazy code
If your map or filter has side effects (like println), they execute lazily — possibly at an unexpected time (or never). Keep lazy operations pure.
5. View conversion back to eager
Calling .toList on a view forces evaluation. If you forget it, you might get a View that recomputes every time you iterate — a performance trap.
What you learned & what's next
You've now explored Scala's lazy evaluation: you can explain the difference between eager and lazy evaluation, and you've applied it with lazy val, view, and LazyList in practical examples. You know when to use each tool and how to sidestep common pitfalls. This connects directly to your goal of becoming a fluent Scala developer — laziness is a cornerstone of functional programming and high-performance JVM apps.
As your next step, you'll dive into pattern matching and case classes, where those lazy collections can be destructured elegantly — think of pattern matching as the switch statement on steroids, working seamlessly with the lazy data structures you just mastered. Get ready to transform the way you handle data in Scala.
Practice recap
Open a Scala REPL and create a LazyList of all natural numbers, then use filter and take to print the first 10 multiples of 3. Next, define a lazy val that simulates a slow computation and observe when it runs. Finally, time a view-based transformation vs. an eager one on a list of 100,000 elements to see the memory impact — you'll be ready to apply laziness in your next project.
Common mistakes
- Forgetting that
LazyListmemoizes — keeping a reference to a large lazy list can cause memory leaks. - Using
.viewbut never converting back to a concrete collection (.toList), causing repeated recomputation on every access. - Placing side effects (
println, file writes) inside lazymap/filter— they may execute at unexpected times or not at all. - Assuming all Scala collections are lazy by default —
List,Vector, andArrayare eager; onlyLazyList,view, andIteratorare lazy.
Variations
- Use
Iteratorfor single-pass lazy traversal without memoization — ideal for reading large files or streams. - The legacy
Streamclass in Scala 2.12 is replaced byLazyListin 2.13; migrate code accordingly. - Consider third-party libraries like Cats Effect's
fs2.Streamfor purely functional streaming with backpressure, if you need more advanced control.
Real-world use cases
- Processing an infinite sequence of sensor readings, taking only those above a threshold with
take. - Lazy-loading expensive configuration or database connections using
lazy valto defer initialization until first use. - Chaining
map/filteron millions of rows in a data pipeline with.viewto avoid creating multiple full-size intermediate collections.
Key takeaways
- Eager evaluation computes immediately; lazy evaluation defers until needed and can avoid wasted work.
- Use
lazy valfor expensive one-time values,.viewfor lazy transforms on finite collections, andLazyListfor infinite sequences. LazyListmemoizes computed elements — balancing memory vs. recomputation.- Laziness adds overhead and can defer errors; use it deliberately for performance, not as a default.
- Beware of side effects in lazy code — they execute at unpredictable times.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.