Skip to Content
Course content

94: Building Lazy Sequences

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

I've been thinking about how we handle large data sets in Kotlin. Most of us default to List, but there's a wall you hit when you don't know how many elements you'll actually need. I wanted to build a utility that generates prime numbers, but I ran into a problem almost immediately.

Why my list of primes is killing my RAM

My first instinct was to write a function that returns a list of primes up to a certain limit. It looks straightforward enough:

fun getPrimes(limit: Int): List {
    val primes = mutableListOf()
    var num = 2
    while (primes.size < limit) {
        if ((2..num/2).all { num % it != 0 }) {
            primes.add(num)
        }
        num++
    }
    return primes
}

This works fine if I want 10 or 100 primes. But what if I'm building a system where I need to pull primes one by one based on some external trigger? Or what if I want to find the first prime that satisfies some complex condition? If I pass 1,000,000 into that function, my app freezes while it calculates every single prime before I can even look at the first one. It's "eager" evaluation, and in this case, it's a bottleneck.

The struggle with manual Iterators

I thought about implementing a custom Iterator. I remember doing this in Java years ago. I'd have to create a class, maintain a state variable for the current number, and override hasNext() and next(). It's a lot of boilerplate for something that's essentially just a loop. I wanted something that felt like a standard function but behaved like a stream.

That's where I started digging into the sequence builder. I wanted to see if I could "pause" my prime-finding loop and only resume it when the caller actually asked for the next value.

The magic of yield

I tried rewriting the logic using the sequence { ... } block. The key here is the yield keyword. Instead of adding to a list, I just "yield" the value. Here is how the code evolved:

fun primeSequence() = sequence {
    var num = 2
    while (true) {
        if ((2..num/2).all { num % it != 0 }) {
            yield(num) // Here is where the magic happens
        }
        num++
    }
}

At first glance, this looks terrifying. There is a while(true) loop! If this were a standard function, it would hang my CPU forever. But I noticed something interesting when I ran it:

val primes = primeSequence() 
println("Sequence created!") 
println(primes.first())

The output is:

  • Sequence created!
  • 2

The loop didn't run until I called .first(). The yield keyword actually suspends the execution of the block. It hands a value back to the caller and then just... stays there. It remembers exactly where it was in the loop, waiting for the next request. It's essentially a state machine generated for me by the compiler.

Testing the laziness

To really prove this to myself, I added a print statement inside the sequence to see when the work was actually happening.

fun debugPrimes() = sequence {
    var num = 2
    while (true) {
        println("Checking $num...") 
        if ((2..num/2).all { num % it != 0 }) {
            yield(num)
        }
        num++
    }
}

val primes = debugPrimes()
println("Requesting first 3 primes:")
primes.take(3).forEach { println("Got $it") }

The output reveals exactly how lazy this is:

Requesting first 3 primes:
Checking 2...
Got 2
Checking 3...
Got 3
Checking 4...
Checking 5...
Got 5

Notice that it stopped at 5. It didn't keep checking numbers up to a million or run infinitely. It did the bare minimum work required to satisfy take(3) and then stopped. By combining sequence with Kotlin's sequence operators (like filter, map, and take), I can build complex data pipelines that handle potentially infinite data without ever blowing up my heap space.




📋 Practical Task

Building a Lazy Fibonacci-Prime Filter

Your task is to create a lazy sequence that generates Fibonacci numbers, but you must wrap it in a pipeline that only yields those Fibonacci numbers that are also prime.

  • Create a function fibonacciSequence() using the sequence { ... } builder that yields Fibonacci numbers infinitely (starting from 0, 1, 1, 2, 3, 5, 8...).
  • Create a second function primeFibonacciSequence() that calls fibonacciSequence() and uses the .filter { ... } operator to keep only the prime numbers.
  • In your main function, use .take(5) to print the first five Fibonacci numbers that are prime.
  • Verification: Ensure that you are not calculating the entire Fibonacci sequence upfront; the output should show that numbers are being processed one by one until the fifth prime Fibonacci is found.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.