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
95: Infinite Sequences with generateSequence
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
generateSequencewith a seed and a lambda for the logic. - Use
takeWhileto ensure the sequence terminates at 1 (inclusive). - The function should handle any positive integer input.
There are no comments for now.