Scala's Lazy Evaluation

Understand Scala's lazy evaluation — Scala for Python Developers.

Focus: scala's lazy evaluation

Sponsored

If you've ever built a data pipeline in Python, you know the pain: you write a transformation chain, and even though only the first few results matter, the entire collection gets computed eagerly, wasting memory and time. Scala's lazy evaluation solves this by deferring computation until absolutely necessary, letting you build infinite sequences and optimize performance. In this lesson, you'll understand how Scala's lazy evaluation works, how it contrasts with Python's generators, and how to use it to write more efficient, scalable code.

The problem this lesson solves

In Python, when you write [x * 2 for x in range(1000000)], the entire list is built in memory — even if you only need the first element. This eager evaluation can blow up memory usage and slow down your application, especially in big-data scenarios. Python offers generators as a workaround, but they can be awkward and require a mental shift. In Scala, lazy evaluation is built into the language, allowing you to define potentially infinite collections and only compute what's actually accessed. This lesson addresses the pain point of unnecessary computation and memory overhead, and shows you how to leverage Scala's lazy collections to write cleaner and more performant code.

Core concept / mental model

Think of Scala's lazy evaluation as a factory conveyor belt that only runs when a product is picked up. The belt can be infinitely long, but you only pull items you need. In programming terms, a lazy collection (like Stream or LazyList) stores the recipe for generating elements, not the elements themselves. When you access an element, the recipe executes just enough to produce that element. This is similar to Python's generators, but it's more integrated into Scala's collection library.

Key definitions:

  • Eager evaluation: Computes all values immediately when the collection is created.
  • Lazy evaluation: Delays computation until a value is actually needed.
  • LazyList (formerly Stream): A lazy, immutable linked list that computes elements on demand.
  • view: A lazy wrapper around an existing collection that defers transformations (like map, filter) until the result is accessed.
  • lazy val: A variable that computes its value only once, on first access.

💡 Pro tip: Think of LazyList as a pipeline that's never fully built. Each element is a small, self-contained computation that runs only when you ask for it — perfect for infinite sequences.

How it works step by step

Let's break down how Scala's lazy evaluation operates:

  1. Define a lazy collection: You create a LazyList using LazyList.from(start) or by calling #:: (the lazy cons operator). At this point, no elements are actually computed.
  2. Apply transformations: You chain map, filter, and other methods on the LazyList. These are non-strict — they build a recipe without executing it.
  3. Access elements: When you call methods like take(n), head, or foreach, the lazy list evaluates only as many elements as needed to produce the result.
  4. Cache computed elements: Once an element is computed, it's cached in the LazyList, so accessing it again doesn't recompute it. This makes repeated access efficient.
  5. Garbage collect prefix if needed: If you force a LazyList into a strict collection, all elements are computed and stored; the lazy list itself may be dropped.

Example sequence:**

  • LazyList.from(1) defines an infinite sequence of integers.
  • .map(_ * 2) creates a new lazy list that doubles each element on demand.
  • .take(5) specifies we only want the first five elements.
  • .toList forces evaluation, which computes exactly five elements.

Note: Stream is deprecated in Scala 2.13 in favor of LazyList. Always use LazyList for new code.

Hands-on walkthrough

Let's write a complete Scala example to demonstrate lazy evaluation. We'll create an infinite LazyList, apply transformations, and observe what gets evaluated.

Example 1: Infinite Fibonacci sequence

// Define an infinite lazy list of Fibonacci numbers
val fibs: LazyList[BigInt] = {
  def loop(a: BigInt, b: BigInt): LazyList[BigInt] = a #:: loop(b, a + b)
  loop(0, 1)
}

// Take first 10 elements and print them
println(fibs.take(10).toList)

Expected output:

List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)

Notice that fibs is infinite — it never runs out of numbers. The #:: operator lazily constructs the next element and the recursive call is deferred. Only when toList forces evaluation does Scala compute exactly the first ten Fibonacci numbers.

Example 2: Lazy view on a large collection

val numbers = (1 to 1000000).toList

// Create a lazy view and apply transformations
val result = numbers.view
  .filter(_ % 2 == 0)
  .map(_ * 3)
  .slice(0, 5)

// Force evaluation and print
println(result.toList)

Expected output:

List(6, 12, 18, 24, 30)

The .view wraps the list and defers filter and map. Without view, the entire list would be filtered and mapped eagerly. With view, only elements needed for the final slice are computed — a huge memory and time saver.

Example 3: Using lazy val for deferred initialization

object Config {
  lazy val settings: Map[String, String] = {
    println("Loading settings...")
    // Simulate heavy I/O
    Map("host" -> "localhost", "port" -> "8080")
  }
}

object Main extends App {
  println("Application starting")
  // Access settings only when needed
  println(Config.settings("host"))
}

Expected output:

Application starting
Loading settings...
localhost

The lazy val delays initialization until first access. If settings is never used, it's never loaded. This is great for expensive resources like database connections or configuration files.

Compare options / when to choose what

Scala offers several ways to achieve laziness. Here's a comparison:

Approach Use case Pros Cons
LazyList Infinite sequences, recursive streams Caches computed elements; ideal for Fibonacci, prime generation Slight overhead for caching; not suitable for random access
view Large existing collections with transformations Avoids intermediate collections; memory efficient Recomputes elements on each traversal unless you force; can cause repeated work if used multiple times
lazy val Expensive initialization Simple; thread-safe (by default) Not a collection; only delays a single value
Iterators (like Python's generators) One-pass traversal Minimal memory Cannot be reused after exhaustion; no caching

When to choose what:

  • Use LazyList when you need to access elements multiple times and/or the sequence is infinite.
  • Use view when you have a large strict collection and want to chain transformations without allocating intermediate collections — but if you traverse the view more than once, it will recompute from the original collection.

  • Use lazy val for single-value lazy initialization, such as loading a config or opening a connection.

  • Use iterators (or Iterator from Scala) when you only need to traverse once and don't want caching (e.g., processing a file line by line).

Troubleshooting & edge cases

1. Stack overflow with deep recursion

If you define a LazyList using recursion that's not tail-recursive, you might hit stack overflow when forcing. For example:

// BAD: Non-tail recursion
val badLazy: LazyList[Int] = 1 #:: badLazy.map(_ + 1)
badLazy.take(10000).toList // StackOverflowError!

Fix: Use tail-recursive functions or a loop to avoid deep recursion chains.

2. View recomputation

Views recompute elements each time you traverse them. If you do val v = list.view.map(f) and then call v.head and v.size, f runs multiple times. This can cause unexpected side effects or performance issues.

Fix: If you need to traverse a view multiple times, force it into a strict collection with .force or .toList after the transformations.

3. Memory retention with LazyList

Because LazyList caches computed elements, holding a reference to the head of a long lazy list prevents garbage collection of the prefix. If you only need to traverse once, consider using an Iterator instead.

4. Mixing lazy and strict collections

When you call .map on a LazyList, the result is lazy. But if you call .toList on a view, it becomes strict. It's easy to accidentally force evaluation too early if you chain these methods incorrectly. Check the return types in your IDE.

What you learned & what's next

You've learned how Scala's lazy evaluation works, including LazyList, views, and lazy val. You now understand how to avoid unnecessary computation, build infinite sequences, and optimize memory usage. You can apply these techniques in data pipelines, configuration loading, and performance-critical applications. As a next step, you'll explore pattern matching in Scala, which pairs beautifully with lazy collections for processing streams of data.

Practice recap

Try building a LazyList that generates all powers of 2 and take the first 20. Then, create a view on a large list and apply a filter and map, and compare the time or memory with a strict version. This hands-on practice will solidify your understanding of when to use lazy evaluation.

Common mistakes

  • Using Stream instead of LazyList in Scala 2.13+ — Stream is deprecated and may cause confusion with future maintenance.
  • Forcing a view multiple times without caching, causing repeated computations and side effects — remember views are recomputed on every traversal.
  • Holding a reference to the head of a long LazyList precludes garbage collection of the prefix — use an Iterator if you only traverse once.
  • Assuming LazyList is strict — forgetting that transformations like map and filter are lazy means elements aren't computed until forced.

Variations

  1. Scala 2.13+ uses LazyList instead of the older Stream; .view is another way to achieve laziness on existing collections.
  2. Use Iterator for one-pass traversal that doesn't cache elements, similar to Python's generators.
  3. lazy val provides per-value laziness for expensive initializations, often used in singletons or configuration objects.

Real-world use cases

  • Generate an infinite sequence of prime numbers for a cryptographic application, computing only the needed primes on demand.
  • Process a huge log file line by line using Iterator or LazyList to keep memory usage constant regardless of file size.
  • Lazy initialization of a database connection pool or configuration registry, deferring costly setup until first actual use.

Key takeaways

  • Scala's lazy evaluation defers computation until the result is actually needed, saving memory and CPU.
  • LazyList is the go-to for infinite sequences; it caches computed elements and allows repeated access.
  • Views on strict collections avoid intermediate allocations but recompute on each traversal unless forced.
  • lazy val delays initialization of a single value until first access, useful for expensive resources.
  • For one-pass processing, prefer Iterator to avoid caching overhead.
  • Always mind recursion depth when building LazyLists; use tail-recursive patterns to avoid stack overflow.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.