Overview

9 Working with laziness

This chapter explores why laziness matters and how to use it effectively in Kotlin to gain efficiency, composability, and the ability to work with potentially infinite data. It contrasts strict and lazy evaluation (e.g., short-circuit Boolean operators and if/else branches) and shows where Kotlin is already lazy. Because Kotlin evaluates function arguments strictly, the chapter motivates “call by need” via memoization rather than mere “call by name,” introducing a reusable Lazy type (a memoized zero-argument function) to deliver true deferred, at-most-once evaluation. Early examples clarify pitfalls of strictness (like rethrowing exceptions when reimplementing ||) and why property delegates and by lazy are not sufficient on their own to defer argument evaluation.

With Lazy as a computational context, the chapter develops practical patterns: composing lazy values with map and flatMap, lifting normal functions so they work over Lazy without forcing evaluation, and sequencing a List> into a single Lazy>. It covers handling errors during deferred evaluation by wrapping results in Result and sequencing them safely, and shows how to apply effects conditionally without prematurely forcing lazy values. The key takeaway is to treat laziness like other modalities (Option, Result, List): you write transformations that describe computations, then decide when—or if—to evaluate.

Building on this, the chapter introduces a lazy list data structure, Stream, whose head and tail are Lazy and can represent infinite sequences. It implements core operations (repeat, iterate, unfold) and shows how to limit, skip, and select elements (takeAtMost, dropAtMost with stack-safe corecursion, takeWhile, dropWhile, exists), fold streams (notably foldRight that builds new lazy structures without immediate evaluation), and define map, filter, append, and flatMap in a way that fuses traversals and minimizes work. Concrete examples include generating Fibonacci numbers and tracing evaluation to observe one-pass processing. The result is a toolkit for expressing clear, functional solutions that remain efficient and feasible even with infinite or expensive computations.

 Summary

  • Strict evaluation means evaluating values as soon as they’re referenced.
  • Lazy evaluation means evaluating values only if and when they’re needed.
  • Some languages are strict, and others are lazy. Some are lazy by default and optionally strict, others are strict by default and optionally lazy.
  • Kotlin is a strict language. It is strict regarding function arguments.
  • Although Kotlin isn’t lazy, you can use the by lazy construct combined with a function to implement laziness.
  • With laziness, you can manipulate and compose infinite data structures.
  • Right folds don’t cause stream evaluation. Only some functions used for folding do.
  • Using folds, you can compose several iterating operations without resulting in multiple iterations.
  • You can easily define and compose infinite streams.

FAQ

What does “laziness” mean in this chapter, and why is it useful?Laziness means deferring computation until its result is actually needed. It improves efficiency, lets you avoid unnecessary work and side effects, and is essential for working with infinite data structures and conditional error handling. In contrast, strict evaluation computes immediately, even if the result is never used.
Is Kotlin strict or lazy? Where does it behave lazily out of the box?Kotlin is strict by default (call-by-value), so function arguments are evaluated before a call. However, it has lazy constructs: - Short-circuit operators || and && (skip the second operand when possible) - if/else (only one branch runs) - Loops and try/catch (handlers run only when needed) - Property delegates (by lazy) and Sequence for deferred evaluation
Why isn’t a function like or(a, b) equivalent to the operator a || b?With a function or(a: Boolean, b: Boolean), Kotlin evaluates both arguments before the call. If b throws, it throws even when a is true. The operator a || b is short-circuiting: when a is true, b is not evaluated. To emulate this, you must pass unevaluated arguments, for example as () -> Boolean or Lazy<Boolean>.
How can I implement “call-by-need” laziness (memoized) in Kotlin?Wrap the computation in a memoized thunk: - Create a Lazy<A> that stores a function () -> A and memoizes its result the first time it’s invoked. - Typical pattern: class Lazy<A>(f: () -> A) : () -> A { private val value by lazy(f); override operator fun invoke() = value } This gives call-by-need (evaluate at first use, then cache).
Is the Kotlin property delegate by lazy enough to keep arguments lazy across function calls?Not by itself. by lazy defers a property’s initialization until first access. But if you pass that property as a normal argument, Kotlin evaluates it at the call site. To keep laziness across function boundaries, make functions accept unevaluated inputs (e.g., () -> T or Lazy<T>) and return Lazy results when appropriate.
How do I compose lazy values without forcing evaluation?Return Lazy results and delay all work inside the returned thunk. Example: - constructMessage(g: Lazy<String>, n: Lazy<String>): Lazy<String> = Lazy { "${g()}, ${n()}!" } - Lift a normal curried function f: (A) -> (B) -> C to lazy inputs: lift2(f): (Lazy<A>) -> (Lazy<B>) -> Lazy<C> = { la -> { lb -> Lazy { f(la())(lb()) } } }
Can I use map/flatMap with Lazy? How about turning List<Lazy<A>> into Lazy<List<A>>?Yes: - map: Lazy<A>.map(f) = Lazy { f(this()) } - flatMap: Lazy<A>.flatMap(f) = Lazy { f(this())() } - sequence: sequence(lst: List<Lazy<A>>): Lazy<List<A>> = Lazy { lst.map { it() } } These preserve laziness and evaluate only when the resulting Lazy is invoked, and then only once.
What’s the right way to deal with exceptions during lazy evaluation?Wrap evaluations in a Result. Example: sequenceResult(lst: List<Lazy<A>>): Lazy<Result<List<A>>> evaluates each thunk inside Result.of and aggregates successes/failures. Using a non-escaping fold may still evaluate all elements; for early stop on the first Failure, use an escaping fold (or an early-return pattern) when building the Result.
What is a lazy Stream here, and how is it different from Kotlin Sequence or Java Stream?The Stream in this chapter is a persistent, memoized, lazily evaluated list: its head and tail are Lazy, so it can model infinite data and reuse previously computed elements. Kotlin Sequence is a generator without memoization; Java Stream is single-use and often stateful. The chapter’s Stream can be reused and partially evaluated, which enables efficient, composable, and repeatable pipelines.
How do I keep lazy streams stack-safe and efficient?- Prefer corecursive (tail-recursive) helpers for operations that skip many elements (e.g., dropAtMost, dropWhile). - takeWhile forces only the first element per step to test the predicate. - foldRight on Stream can be safe if the combining function builds new lazy descriptions (no eager evaluation). But filter via foldRight may overflow on long “no-match” runs; fix by first dropWhile {!p(x)} and then cons the head lazily, or use a stack-safe approach that removes long false prefixes.
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Joy of Kotlin ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Joy of Kotlin ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Joy of Kotlin ebook for free