liveBook · Manning

Lazy evaluation

As explained in Functional Programming in Scala, Second Edition

Lazy evaluation defers evaluating an expression until its value is required.

Lazy evaluation defers an expression until its value is demanded. In strict code, a chain such as `map`, `filter`, and `map` traverses a list repeatedly, allocating intermediate lists that are immediately discarded. Laziness keeps the same compositional, higher-order style while allowing transformations to produce results only as a consumer requests them. section 5.0 section 5.6

Listing 5.1 Program trace for List
List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)
List(11,12,13,14).filter(_ % 2 == 0).map(_ * 3)
List(12,14).map(_ * 3)
List(36,42)

The trace exposes the strict cost: each operation first constructs a complete list for the next operation. A lazy representation can instead interleave production, filtering, and consumption. section 5.0

A lazy list represents its head and tail as thunks—functions with no arguments that delay a computation. Forcing a thunk obtains its value. The `cons` smart constructor stores the head and tail in `lazy val`s, so a forced result is cached rather than recomputed. This permits a list to describe more elements than have yet been evaluated. section 5.2 section 5.6

Stopping when the answer is known

`exists(p)` tests the current head, then uses nonstrict `||` to avoid evaluating the tail when `p(h())` is true. A lazy `foldRight` generalizes this: its second argument is the unevaluated fold of the tail, and a combining function may discard it. `forAll`, `find`, `takeWhile`, and filtering can use the same ability to terminate or preserve only the demanded portion. section 5.3 section 5.6

This is more than an optimization. Laziness separates a computation’s description from the decision of how much to run. That separation makes one description reusable in contexts demanding different portions of its result, and it makes productive infinite sequences practical when consumers use operations such as `take`, `exists`, or lazy `foldRight`. section 5.3 section 5.5 section 5.6

Nonstrict parser combinators

Nonstrictness also matters when deferred work prevents divergence. A recursive parser `many` cannot use a strict second argument to `map2`, because constructing that argument recursively expands forever before parsing begins. Making `product`, `map2`, and left-biased `or` nonstrict in their second parser delays it until it is actually needed. section 9.2.1

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Defines thunks, forcing, memoization, and by-name parameters, then develops lazy-list operations and parser-combinator nonstrictness.
Examples
Traces chained List and LazyList transformations; implements exists, take, map, filter, infinite streams, unfold, and parser many.
What it assumes
Builds from List operations and higher-order functions while introducing nonstrictness, thunks, forcing, and lazy-list representation explicitly.
What you can do after
Build it from scratch with thunk-backed LazyList constructors and apply nonstrict folds to stop traversal early.
How it connects
Connects strictness, nonstrictness, thunks, memoization, LazyList, foldRight, infinite sequences, and parser combinators.

Also explained in

Compare how 13 books explain lazy evaluation →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.