Kotlin's Iterable and Sequence look exactly same. Why are two types required?
The key difference lies in the semantics and the implementation of the stdlib extension functions for Iterable<T>
and Sequence<T>
.
For
Sequence<T>
, the extension functions perform lazily where possible, similarly to Java Streams intermediate operations. For example,Sequence<T>.map { ... }
returns anotherSequence<R>
and does not actually process the items until a terminal operation liketoList
orfold
is called.Consider this code:
val seq = sequenceOf(1, 2) val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate print("before sum ") val sum = seqMapped.sum() // terminal
It prints:
before sum 1 2
Sequence<T>
is intended for lazy usage and efficient pipelining when you want to reduce the work done in terminal operations as much as possible, same to Java Streams. However, laziness introduces some overhead, which is undesirable for common simple transformations of smaller collections and makes them less performant.In general, there is no good way to determine when it is needed, so in Kotlin stdlib laziness is made explicit and extracted to the
Sequence<T>
interface to avoid using it on all theIterable
s by default.For
Iterable<T>
, on contrary, the extension functions with intermediate operation semantics work eagerly, process the items right away and return anotherIterable
. For example,Iterable<T>.map { ... }
returns aList<R>
with the mapping results in it.The equivalent code for Iterable:
val lst = listOf(1, 2) val lstMapped: List<Int> = lst.map { print("$it "); it * it } print("before sum ") val sum = lstMapped.sum()
This prints out:
1 2 before sum
As said above,
Iterable<T>
is non-lazy by default, and this solution shows itself well: in most cases it has good locality of reference thus taking advantage of CPU cache, prediction, prefetching etc. so that even multiple copying of a collection still works good enough and performs better in simple cases with small collections.If you need more control over the evaluation pipeline, there is an explicit conversion to a lazy sequence with
Iterable<T>.asSequence()
function.
Completing hotkey's answer:
It is important to notice how Sequence and Iterable iterates throughout your elements:
Sequence example:
list.asSequence().filter { field ->
Log.d("Filter", "filter")
field.value > 0
}.map {
Log.d("Map", "Map")
}.forEach {
Log.d("Each", "Each")
}
Log result:
filter - Map - Each; filter - Map - Each
Iterable example:
list.filter { field ->
Log.d("Filter", "filter")
field.value > 0
}.map {
Log.d("Map", "Map")
}.forEach {
Log.d("Each", "Each")
}
filter - filter - Map - Map - Each - Each
Iterable
is mapped to thejava.lang.Iterable
interface on theJVM
, and is implemented by commonly used collections, like List or Set. The collection extension functions on these are evaluated eagerly, which means they all immediately process all elements in their input and return a new collection containing the result.Here’s a simple example of using the collection functions to get the names of the first five people in a list whose age is at least 21:
val people: List<Person> = getPeople() val allowedEntrance = people .filter { it.age >= 21 } .map { it.name } .take(5)
Target platform: JVMRunning on kotlin v. 1.3.61 First, the age check is done for every single Person in the list, with the result put in a brand new list. Then, the mapping to their names is done for every Person who remained after the filter operator, ending up in yet another new list (this is now a
List<String>
). Finally, there’s one last new list created to contain the first five elements of the previous list.In contrast, Sequence is a new concept in Kotlin to represent a lazily evaluated collection of values. The same collection extensions are available for the
Sequence
interface, but these immediately return Sequence instances that represent a processed state of the date, but without actually processing any elements. To start processing, theSequence
has to be terminated with a terminal operator, these are basically a request to the Sequence to materialize the data it represents in some concrete form. Examples includetoList
,toSet
, andsum
, to mention just a few. When these are called, only the minimum required number of elements will be processed to produce the demanded result.Transforming an existing collection to a Sequence is pretty straightfoward, you just need to use the
asSequence
extension. As mentioned above, you also need to add a terminal operator, otherwise the Sequence will never do any processing (again, lazy!).val people: List<Person> = getPeople() val allowedEntrance = people.asSequence() .filter { it.age >= 21 } .map { it.name } .take(5) .toList()
Target platform: JVMRunning on kotlin v. 1.3.61 In this case, the Person instances in the Sequence are each checked for their age, if they pass, they have their name extracted, and then added to the result list. This is repeated for each person in the original list until there are five people found. At this point, the toList function returns a list, and the rest of the people in the
Sequence
are not processed.There’s also something extra a Sequence is capable of: it can contain an infinite number of items. With this in perspective, it makes sense that operators work the way they do - an operator on an infinite sequence could never return if it did its work eagerly.
As an example, here’s a sequence that will generate as many powers of 2 as required by its terminal operator (ignoring the fact that this would quickly overflow):
generateSequence(1) { n -> n * 2 } .take(20) .forEach(::println)
You can find more here.