Skip to Content
Course content

95: Infinite Sequences with generateSequence

Click on the "Edit" button in the top corner of the screen to edit your slide content.

At some point in your career, you'll need to generate a series of values where you don't actually know how many you need upfront. Maybe you're calculating exponential backoff intervals for a network retry, or you're traversing a linked data structure. The instinct for many developers is to just build a list. It's the safest, most familiar tool in the box.

The Eager List Trap

Let's say we want to generate a sequence of power-of-two buffer sizes (2, 4, 8, 16...) but we only want to stop once we've hit a size that can accommodate a specific piece of data. A naive approach looks like this:

fun getBufferSizes(target: Int): List<Int> {
    val sizes = mutableListOf<Int>()
    var current = 2
    while (current <= target) {
        sizes.add(current)
        current *= 2
    }
    return sizes
}

This works fine for small numbers, but it's fundamentally "eager." You're allocating a MutableList, growing that list in memory, and calculating every single value before you even return the result to the caller. If target is massive, or if the logic for generating the next number is computationally expensive, you're wasting cycles and RAM on values that the caller might not even use. I've seen this lead to OutOfMemoryErrors in production because someone assumed the sequence would stay small, only for a weird edge case to push the loop into the millions.

Switching to Lazy Generation

This is where generateSequence comes in. Instead of building a collection, it creates a Sequence. In Kotlin, sequences are lazy. They don't store the values; they store the instruction on how to get the next value. Here is how I'd rewrite that logic to be more professional and efficient:

val bufferSequence = generateSequence(2) { it * 2 }

That's it. That single line defines a potentially infinite sequence. The 2 is the seed (the first element), and the lambda { it * 2 } is the rule for calculating the next element based on the previous one. If you print bufferSequence right now, you won't see a list of numbers; you'll see a Sequence object. Nothing has been calculated yet. I love this because it decouples the generation of the data from the consumption of the data.

Managing the Infinite

Since generateSequence can theoretically run forever, you can't just call .toList() on it unless you know for a fact it will terminate. If you do that with the example above, your program will hang until it hits an integer overflow or crashes. You have to tell the sequence when to stop using a "terminal operation" or a limiting operator.

To replicate our previous logic, we can use takeWhile:

val target = 1000
val result = generateSequence(2) { it * 2 }
    .takeWhile { it <= target }
    .toList()

The magic here is that the sequence only calculates the next power of two when takeWhile asks for it. Once takeWhile sees a value greater than 1000, it tells the sequence to stop, and the process terminates. You've avoided unnecessary allocations and shifted the control of "how much data I need" to the person actually using the sequence. It's a much cleaner contract between your data provider and your data consumer.




📋 Practical Task

Implementing a Collatz Conjecture Sequence

The Collatz Conjecture is a famous mathematical sequence: start with any positive integer n. If n is even, the next number is n / 2. If n is odd, the next number is 3n + 1. The sequence always eventually reaches 1.

Your task is to write a function called collatzSequence(start: Int) that uses generateSequence to produce this series. The sequence should stop exactly when it reaches the value 1. Return the result as a List<Int>.

Requirements:

  • Use generateSequence with a seed and a lambda for the logic.
  • Use takeWhile to ensure the sequence terminates at 1 (inclusive).
  • The function should handle any positive integer input.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.