In terms of high-level functionalities, the Sequence and Collection data structures are nearly the same. They both allow iteration through their elements. There are also many powerful extension functions in the Kotlin standard library that provide declarative-style data-processing operations for each of them. However, the Sequence data structure behaves differently under the hood—it delays any operations on its elements until they are finally consumed. It instantiates the subsequent elements on the go while traversing through them. These characteristics of Sequence, called lazy evaluation, make this data structure quite similar to the Java concept, Stream. To understand all of this better, we are going to implement a simple data-processing scenario to analyze the efficiency and behavior of Sequence and contrast our findings with Collection-based implementation.
Discovering the concept of sequences
Getting ready
Let's consider the following example:
val collection = listOf("a", "b", "c", "d", "e", "f", "g", "h")
val transformedCollection = collection.map {
println("Applying map function for $it")
it
}
println(transformedCollection.take(2))
In the first line, we created a list of strings and assigned it to the collection variable. Next, we are applying the map() function to the list. Mapping operation allows us to transform each element of the collection and return a new value instead of the original one. In our case, we are using it just to observe that map() was invoked by printing the message to the console. Finally, we want to filter our collection to contain only the first two elements using the take() function and print the content of the list to the console.
In the end, the preceding code prints the following output:
Applying map function for a
Applying map function for b
Applying map function for c
Applying map function for d
Applying map function for e
Applying map function for f
Applying map function for g
Applying map function for h
[a, b]
As you can see, the map() function was properly applied to every element of the collection and the take() function has properly filtered the elements of the list. However, it would not be an optimal implementation if we were working with a larger dataset. Preferably, we would like to wait with the execution of the data-processing operations until we know what specific elements of the dataset we really need, and then apply those operations only to those elements. It turns out that we can easily optimize our scenario using the Sequence data structure. Let's explore how to do it in the next section.
How to do it...
- Declare a Sequence instance for the given elements:
val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")
- Apply the mapping operation to the elements of the sequence:
val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")
val transformedSequence = sequence.map {
println("Applying map function for $it")
it
}
- Print the first two elements of the sequence to the console:
val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")
val transformedSequence = sequence.map {
println("Applying map function for $it")
it
}
println(transformedSequence.take(2).toList())
How it works...
The Sequence-based implementation is going to give us the following output:
Applying map function for a
Applying map function for b
[a, b]
As you can see, replacing the Collection data structure with the Sequence type allows us to gain the desired optimization.
The scenario considered in this recipe was implemented identically—first, using List, then using the Sequence type. However, we can notice the difference in the behavior of the Sequence data structure compared to that of Collection. The map() function was applied only to the first two elements of the sequence, even though the take() function was called after the mapping transformation declaration. It's also worth noting that in the example using Collection, the mapping was performed instantly when the map() function was invoked. In the case of Sequence, mapping was performed at the time of the evaluation of its elements while printing them to the console, and more precisely while converting Sequence to the List type with the following line of code:
println(transformedSequence.take(2).toList())
There's more...
There is a convenient way of transforming Collection to Sequence. We can do so with the asSequence() extension function provided by the Kotlin standard library for the Iterable type. In order to convert a Sequence instance into a Collection instance, you can use the toList() function.
See also
- Thanks to the feature of Sequence lazy evaluation, we have avoided needless calculations, increasing the performance of the code at the same time. Lazy evaluation allows the implementation of sequences with a potentially infinite number of elements and turns out to be effective when implementing algorithms as well.
- You can explore a Sequence-based implementation of the Fibonacci algorithm in the Applying sequences to solve algorithmic problems recipe. It presents, in more detail, another useful function for defining sequences called generateSequence().