Skip to main content

Kotlin Sequences

Have you ever worked with large collections in Kotlin and noticed your program slowing down when performing multiple operations? Kotlin Sequences might be the solution you're looking for. In this tutorial, we'll explore how Sequences can optimize the way you process collections through lazy evaluation and improved performance.

Introduction to Sequences

Sequences are a fundamental concept in Kotlin's functional programming toolkit. They represent a lazily-evaluated collection of elements that only processes items when needed. This is in contrast to Kotlin's standard collections (like List, Set) that process elements eagerly.

The key advantage of sequences is that they perform operations one element at a time through the entire chain of operations, rather than processing the entire collection at each step.

Creating Sequences

Let's start by looking at different ways to create sequences in Kotlin:

From Collections

kotlin
val listSequence = listOf(1, 2, 3, 4, 5).asSequence()

Using generateSequence()

kotlin
// Creating an infinite sequence
val infiniteNumbers = generateSequence(1) { it + 1 }
// Taking first 5 elements
val firstFiveNumbers = infiniteNumbers.take(5).toList()
println(firstFiveNumbers) // Output: [1, 2, 3, 4, 5]

Using sequence() builder

kotlin
val fibonacciSequence = sequence {
var pair = 0 to 1
while (true) {
yield(pair.first)
pair = pair.second to pair.first + pair.second
}
}

println(fibonacciSequence.take(8).toList()) // Output: [0, 1, 1, 2, 3, 5, 8, 13]

Eager vs Lazy Evaluation

To understand why sequences are useful, let's compare eager evaluation (standard collections) with lazy evaluation (sequences):

kotlin
// Eager evaluation with collections
val result1 = listOf(1, 2, 3, 4, 5)
.map {
println("Map: $it")
it * 2
}
.filter {
println("Filter: $it")
it > 5
}
.first()
println("Result: $result1")

// Lazy evaluation with sequences
val result2 = listOf(1, 2, 3, 4, 5)
.asSequence()
.map {
println("Map sequence: $it")
it * 2
}
.filter {
println("Filter sequence: $it")
it > 5
}
.first()
println("Result: $result2")

Output:

Map: 1
Map: 2
Map: 3
Map: 4
Map: 5
Filter: 2
Filter: 4
Filter: 6
Filter: 8
Filter: 10
Result: 6

Map sequence: 1
Filter sequence: 2
Map sequence: 2
Filter sequence: 4
Map sequence: 3
Filter sequence: 6
Result: 6

Notice the difference? With standard collections, all elements are mapped first, then filtered. With sequences, each element goes through the entire processing chain before moving to the next element, stopping as soon as a result is found.

Common Sequence Operations

Sequences support the same operations as collections:

Transformations

kotlin
val doubled = sequenceOf(1, 2, 3)
.map { it * 2 }
.toList()
println(doubled) // Output: [2, 4, 6]

Filtering

kotlin
val evenNumbers = sequenceOf(1, 2, 3, 4, 5)
.filter { it % 2 == 0 }
.toList()
println(evenNumbers) // Output: [2, 4]

Chaining Operations

kotlin
val result = sequenceOf(1, 2, 3, 4, 5)
.map { it * it } // Square each number
.filter { it > 10 } // Keep only values greater than 10
.map { it * 2 } // Double each remaining value
.toList()
println(result) // Output: [32, 50]

When to Use Sequences

Sequences are particularly useful in the following scenarios:

  1. Processing large collections: When dealing with large amounts of data
  2. Multiple operation chains: When performing several operations in sequence
  3. Short-circuiting operations: When using operations like first(), find(), or take(n)
  4. Avoiding intermediate collection creation: To reduce memory usage

Real-World Example: Log File Processing

Imagine you need to process a large log file to find specific error messages:

kotlin
fun processLogFile(logLines: List<String>): List<String> {
return logLines
.asSequence()
.filter { it.contains("ERROR", ignoreCase = true) }
.map { it.substringAfter(": ").trim() }
.filter { it.length > 10 }
.take(5)
.toList()
}

// Sample usage
val sampleLogs = listOf(
"INFO: Application started",
"ERROR: Database connection failed",
"DEBUG: Checking settings",
"ERROR: Invalid user credentials provided",
"ERROR: Network timeout occurred",
"WARN: Low memory warning",
"ERROR: File system permission denied",
"ERROR: API rate limit exceeded"
)

val errors = processLogFile(sampleLogs)
println("Top 5 error messages:")
errors.forEach { println("- $it") }

Output:

Top 5 error messages:
- Database connection failed
- Invalid user credentials provided
- Network timeout occurred
- File system permission denied
- API rate limit exceeded

This example processes the log file efficiently, looking for error messages without creating intermediate collections for each operation.

Performance Considerations

While sequences can improve performance, they're not always the best choice:

  • For small collections, regular collection operations may be faster due to less overhead
  • Sequences have overhead for setting up the lazy evaluation machinery
  • Sequences shine when you have multiple operations and large datasets

Infinite Sequences

One of the powerful features of sequences is the ability to create infinite sequences:

kotlin
// Generate an infinite sequence of powers of 2
val powersOfTwo = generateSequence(1) { it * 2 }

// Get the first 10 powers of 2
val firstTenPowers = powersOfTwo.take(10).toList()
println(firstTenPowers) // Output: [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]

// Create a sequence of dates starting from today
val dateSequence = generateSequence(LocalDate.now()) { it.plusDays(1) }
val nextWeek = dateSequence.take(7).toList()
println("Next week's dates: $nextWeek")

Sequence vs Collection Operations

Here's a summary of the key differences:

AspectSequencesCollections
EvaluationLazy (one element at a time)Eager (entire collection)
Intermediate collectionsNone createdCreated at each step
Performance with large dataBetterWorse
Performance with small dataSlightly worseBetter
Memory usageLowerHigher
Short-circuit operationsVery efficientLess efficient

Summary

Kotlin Sequences are a powerful tool for efficiently processing collections through lazy evaluation. They:

  • Process elements one at a time through the entire operation chain
  • Avoid creating intermediate collections
  • Can improve performance with large datasets
  • Work well with short-circuiting operations
  • Can represent infinite sequences of data

Next time you're working with collections in Kotlin, consider whether using a sequence might improve your code's performance and readability, especially when dealing with large datasets or multiple chained operations.

Exercises

  1. Create a sequence that generates the first 20 Fibonacci numbers
  2. Process a list of 1000 numbers, finding the sum of the squares of all even numbers using both standard collections and sequences. Compare the performance.
  3. Create an infinite sequence of random numbers between 1 and 100, then find the first sequence of three consecutive numbers that sum to more than 200
  4. Implement a word frequency counter that uses sequences to process a large text file

Additional Resources

Happy coding with Kotlin Sequences!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)