Lazy evaluation is an evaluation strategy that postpones computing an expression until its value is actually demanded, and then remembers the result so the work is never repeated. In functional programming, it allows programs to describe potentially infinite data structures, to compose pipelines without building intermediate collections and to skip computations whose results are never used.
The opposite strategy, eager or strict evaluation, computes every argument before a function body runs, which is the default in most mainstream languages. Haskell is the best-known language that is lazy by default, while languages such as Scala, F#, Clojure, Kotlin and Python offer laziness selectively through streams, sequences, generators and lazy values.
How lazy evaluation works with thunks
A lazy runtime replaces each unevaluated expression with a thunk, a small object that holds the code needed to compute the value and the environment that code refers to. When the program first forces the thunk, for example by pattern matching on it or printing it, the runtime executes the code and overwrites the thunk with its result. Later accesses read the stored value directly.
This combination of delay and caching is called call-by-need. It differs from call-by-name, which also delays evaluation but recomputes the expression every time it is used. Call-by-need is safe only when expressions are free of side effects, because the program cannot predict when or whether a thunk will run. That is why laziness pairs naturally with pure functions and referential transparency.
The table below places the three classic parameter-passing strategies side by side, since lazy evaluation is best understood as one point in this design space.
| Strategy | When arguments are evaluated | Repeated use | Example languages |
|---|---|---|---|
| Call-by-value | Before the function is entered | Computed once | Java, Python, OCaml, Rust |
| Call-by-name | At each use inside the body | Recomputed each time | Algol 60, Scala by-name parameters |
| Call-by-need | At first use, then cached | Computed once | Haskell, lazy values in Scala and Kotlin |
What laziness makes possible
The main benefit of lazy evaluation is separating the description of data from its consumption. A producer can define every prime number or every line of a huge log file, and a consumer decides how much of it to take. In Haskell, take 5 (filter even [1..]) terminates because the list [1..] is materialized only as far as the consumer asks.
- Infinite structures: streams of events, numbers or game states that are generated on demand.
- Short-circuiting: operations such as
any,findor&&stop as soon as the answer is known. - Pipeline fusion: chained
map,filterandtakesteps process one element at a time without intermediate lists. - Custom control structures: ordinary functions can act like
ifbecause unused branches are never evaluated. - Memoized definitions: a value defined in terms of itself, such as a dynamic programming table, fills in only the cells that are requested.
Laziness also improves modularity. Without it, a search routine must interleave generating candidates with testing them in a single loop. With it, the generator and the test become independent functions that are composed afterward, and each can be reused or tested alone.
Lazy evaluation across languages
Most languages are strict by default and expose laziness through specific constructs, so the strategy appears in different shapes depending on the ecosystem. The following table summarizes the most common forms.
| Language | Lazy construct | Scope of laziness |
|---|---|---|
| Haskell | Default semantics | Every expression unless forced |
| Scala | lazy val, LazyList, by-name parameters | Opt-in per value or collection |
| Kotlin | by lazy, Sequence | Opt-in per property or pipeline |
| Python | Generators, itertools, generator expressions | Iteration only, not cached |
| Java | Stream API, Supplier | Intermediate stream operations |
| C# | LINQ with yield return, Lazy<T> | Deferred query execution |
Note that Python generators and Java streams are single-pass and uncached: iterating twice either fails or recomputes everything, which is closer to call-by-name than to true call-by-need. A Haskell list, by contrast, is shared and evaluated at most once, no matter how many consumers read it.
Costs and the problem of space leaks
Laziness is not free. Each thunk occupies heap memory and adds an indirection, and the runtime must check whether a value has already been computed. In tight numeric loops this overhead can make lazy code several times slower than its strict equivalent, which is why optimizing compilers such as GHC perform strictness analysis to evaluate eagerly wherever the result is certain to be needed.
The more serious risk is the space leak. A classic case is summing a large list with a lazy left fold: instead of a running total, the program builds a chain of millions of unevaluated additions, ((0 + 1) + 2) + ..., and may exhaust memory or the stack before forcing it. The remedy is a strict accumulator, such as foldl' in Haskell, or bang patterns that force values at each step.
| Symptom | Likely cause | Typical remedy |
|---|---|---|
| Memory grows steadily, then crashes | Accumulated thunks in a fold | Use a strict fold or strict fields |
| File closed before data is read | Lazy I/O outliving its handle | Use streaming libraries or force the content |
| Unpredictable timing of logs | Side effects inside lazy code | Keep effects in the I/O layer |
| Large structure kept alive | Thunk retaining a reference | Force the value or restructure sharing |
| Hard-to-read profiles | Cost shifted to the consumer | Use cost-center profiling |
Laziness also makes performance harder to reason about, because the cost of an expression is paid wherever it is first forced, which may be far from where it was written. Experienced developers therefore mix strategies deliberately: lazy for structure, strict for accumulation.
When to choose lazy or eager evaluation
The right choice depends on how much of the data will be consumed and how expensive each element is. The decision table below summarizes guidance that holds in most functional code bases.
| Situation | Recommended strategy | Reason |
|---|---|---|
| Large or unbounded input, partial consumption | Lazy | Avoids work on unused elements |
| Numeric accumulation over many items | Strict | Prevents thunk buildup |
| Expensive value that may never be used | Lazy with caching | Pays the cost only on demand |
| Code with logging, I/O or mutation | Strict | Keeps effects in a predictable order |
| Small collections used completely | Strict | Lower overhead, simpler profiling |
Put simply
Lazy evaluation means doing the work only when someone actually needs the answer. Imagine a restaurant that cooks a dish only after a guest orders it, instead of preparing every item on the menu at opening time. Nothing is wasted on dishes nobody wants, and once a dish is ready, a second guest asking for the same plate from the same batch is served immediately.
The catch is that the kitchen must keep track of every pending order. If orders pile up faster than they are cooked, the list itself becomes a problem, which is the software version of running out of memory.
In practice
A data engineer needs the first 20 server log entries that contain a specific error code from a 40 GB archive. An eager script that reads the whole file into a list, filters it and then takes 20 items would need tens of gigabytes of memory and several minutes of reading.
Rewritten as a lazy pipeline, using a generator that yields one line at a time, a filter on the error code and a take of 20 results, the program reads lines only until the twentieth match appears. If the matches occur in the first few megabytes, the job finishes in well under a second with almost constant memory. The engineer also reuses the same generator and filter in another report that counts matches per hour, because the stages are independent functions.