4 Recursion, corecursion, and memoization
This chapter explains how to think and program with recursion and corecursion in Kotlin, and why the language’s tail call elimination makes corecursive (tail-recursive) solutions both expressive and safe. It contrasts recursive processes that defer work and consume stack space with corecursive ones that evaluate each step immediately and can be compiled to loops when marked tailrec. The chapter shows how to turn intuitive but stack-unsafe recursive definitions into tail-recursive implementations by introducing helper functions, accumulators, and local functions, and it emphasizes choosing corecursion whenever possible to avoid stack overflows while preserving clarity and intent.
Building on that foundation, the chapter develops a toolkit of list-processing patterns. It starts from head/tail decomposition and progressively abstracts toward reusable folds: foldLeft (corecursive and stack-safe) and foldRight (naturally recursive and potentially unsafe). With these, it refactors common tasks—summing lists, joining elements into strings, reversing lists, mapping—into concise, reusable combinators. It also demonstrates how to transform loop-based flows into functional constructs such as range, unfold, and iterate, and turns the naive, exponential Fibonacci into a fast, tail-recursive BigInteger version. Along the way, it highlights the difference between strict list processing and the laziness of control structures, noting that truly lazy collections arrive later in the book.
The chapter closes with memoization as a complementary optimization: caching results to avoid recomputation. It shows implicit memoization (building intermediate lists you can reuse) and explicit memoization via maps, then generalizes with a thread-safe Memoizer that turns any single-argument function into a memoized one. It extends the approach to multi-argument functions via currying or tuples, discusses purity (memoized functions remain extensionally pure; only time varies), and warns about practical concerns such as memory growth and when to consider weak/soft references. Combined with tail recursion, folds, and unfold/iterate patterns, memoization provides a pragmatic path to safe, expressive, and efficient functional Kotlin.
Figure 4.1. Corecursively transforming a list of characters into a string
Figure 4.2. Recursively transforming a list of characters into a string
Figure 4.3. A corecursive computation
Figure 4.4. A recursive computation
Figure 4.5. A flow chart of the imperative computation of the sum of n first integers
Summary
- A recursive function calls itself, using this call to itself as an element for further computation.
- Recursive functions push the current computation state onto the stack before recursively calling themselves.
- The Kotlin default stack size is limited. If the number of recursive steps is too high, you’ll get a
StackOverflowException.
- Tail-recursive functions are functions in which the recursive call is in the last (tail) position.
- In Kotlin, tail-recursive functions are optimized using Tail Call Elimination (TCE).
- Lambdas can be made recursive.
- Memoization allows functions to remember their computed result in order to speed up later access.
- Memoization can be made automatic.
FAQ
What’s the difference between recursion and corecursion?
Recursion builds a result by deferring work until a terminal condition is reached, then it evaluates the stacked steps in reverse order. Corecursion evaluates each step immediately, passing the partial result to the next step. Practical consequences: - Corecursion can run in constant stack space and be compiled into a loop (with tailrec). - Recursion stores intermediate steps on the call stack and risks stack overflow. Example: Joining a List<Char> to String - Corecursive: start with "" and append each char. - Recursive: process to the end and prepend each char on the way back.What is Tail Call Elimination (TCE) in Kotlin and how do I use it?
Tail Call Elimination turns a tail-recursive function into an iterative loop, preventing stack growth. In Kotlin: - Mark the function with the tailrec keyword. - The self-call must be in tail position (the last action; its result is returned directly). - Kotlin verifies this; if not tail-recursive, it warns you. Example:tailrec fun sumTail(n: Int, acc: Int = 0): Int =
if (n == 0) acc else sumTail(n - 1, acc + n)
This compiles to a loop and avoids StackOverflowError.How do I turn a non‑tail‑recursive function into a tail‑recursive one?
Introduce an accumulator and a local helper function: 1) Start with a simple recursive definition (e.g., sum(1..n)):fun sum(n: Int): Int = if (n == 0) 0 else n + sum(n - 1)2) Add a helper that carries the partial result:
fun sum(n: Int): Int {
tailrec fun go(i: Int, acc: Int): Int =
if (i == 0) acc else go(i - 1, acc + i)
return go(n, 0)
}
Pattern:
- Move work into parameters.
- Ensure the self-call is the last expression.Why is the naive recursive Fibonacci so slow, and how can I fix it?
The naive version is doubly recursive (f(n-1) + f(n-2)), causing about 2^n calls—astronomical growth—so it “hangs” rather than overflows the stack quickly. Fixes: - Tail-recursive with two accumulators (BigInteger), e.g., carry f(n−1) and f(n−2). - Corecursive approach: iterate over pairs (a, b) ↦ (b, a+b), collect firsts. Both approaches run in linear time and constant stack space (with tailrec).How should I process lists with recursion in Kotlin without blowing the stack?
Split the list into head and tail, but use tail recursion with an accumulator: - Provide safe head/tail helpers or extensions. - Move computation into an accumulator and place the self-call in tail position. Example (sum):fun sum(list: List<Int>): Int {
tailrec fun go(xs: List<Int>, acc: Int): Int =
if (xs.isEmpty()) acc else go(xs.drop(1), acc + xs.first())
return go(list, 0)
}
Better yet, abstract with foldLeft to avoid writing recursion directly.What are foldLeft and foldRight, and when should I use each?
- foldLeft(list, z, f): corecursive/left fold; processes left-to-right, tail-recursive, stack-safe. - foldRight(list, z, f): recursive/right fold; processes right-to-left, not tail-recursive (may overflow on long lists). Definitions (sketch):fun <T, U> foldLeft(xs: List<T>, z: U, f: (U, T) -> U): U {
tailrec fun go(ys: List<T>, acc: U): U =
if (ys.isEmpty()) acc else go(ys.drop(1), f(acc, ys.first()))
return go(xs, z)
}
Prefer foldLeft for stack safety. Emulate foldRight via reverse + foldLeft if needed.How can I build index lists and other series without loops?
Use range, unfold, or iterate: - range(start, end): [start, start+1, ..., end-1] - unfold(seed, f, p): generate seed, seed’=f(seed), ... while p(current) is true. - iterate(seed, f, n): generate n elements by repeated f. Examples:fun range(s: Int, e: Int) = unfold(s, { it + 1 }, { it < e })
fun <T> unfold(seed: T, f: (T)->T, p: (T)->Boolean): List<T> {
tailrec fun go(acc: List<T>, x: T): List<T> =
if (!p(x)) acc else go(acc + x, f(x))
return go(emptyList(), seed)
}
Note: Lists are strict; you build all elements eagerly.What is memoization and how do I implement it generically in Kotlin?
Memoization caches results of f(x) so repeated calls with the same x return instantly. Generic approach: - Wrap a function with a cache (e.g., ConcurrentHashMap) using computeIfAbsent. - Provide a helper to memoize any (T) → U:class Memoizer<T,U> private constructor() {
private val cache = java.util.concurrent.ConcurrentHashMap<T,U>()
private fun doMemoize(f: (T)->U): (T)->U = { t -> cache.computeIfAbsent(t, f) }
companion object { fun <T,U> memoize(f: (T)->U): (T)->U = Memoizer<T,U>().doMemoize(f) }
}
Multi-arg functions: memoize the curried form or accept a tuple (Pair/Triple/custom).
The Joy of Kotlin ebook for free