Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
94: Building Lazy Sequences
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 thesequence { ... }builder that yields Fibonacci numbers infinitely (starting from 0, 1, 1, 2, 3, 5, 8...). - Create a second function
primeFibonacciSequence()that callsfibonacciSequence()and uses the.filter { ... }operator to keep only the prime numbers. - In your
mainfunction, 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.
There are no comments for now.