Comparison between Kotlin iteration using toList and asIterable

In this previous post, I discussed Kotlin’s Iterables and Sequences, focusing on the eager versus deferred and lazy behavior of their extension functions (see this helpful this StackOverflow discussion). I came across a statement that “Collections (classes implementing Iterable) contain items, while Sequences produce items,” which resonated with me. However, I didn’t address the peculiar fact that Sequences in Kotlin aren’t Iterables (I don’t see why the Sequence<T> interface couldn’t implement the Iterable<T> interface – it wouldn’t interfere with utilizing the correct extension function).

In languages like Python and JavaScript (which lack an explicit Iterable class or interface), an iterable is essentially a “protocol.” If you can obtain an iterator from it, it’s iterable. Kotlin has an Iterable interface, yet some things, like Sequences, are iterable (both Iterable and Sequence interfaces have an iterator() method) without implementing that interface. The confusion stems from saying “X is iterable,” which sounds deceptively similar to “X is (implements) an Iterable.” “X can be iterated” seems more accurate – a Sequence can be iterated but isn’t an Iterable.

Semantics aside, let’s get practical. We can derive a Sequence from an Iterable using the asSequence() extension function or the Sequence() function. This is beneficial when we want to leverage lazy extension functions for operations like mapping and filtering. Conversely, we can “materialize” a Sequence into a Collection using the toList() extension function, forcing the Sequence to generate all its items and store them within a collection. We also have the Sequence[T].asIterable() extension function at our disposal. When and why would we opt for asIterable() over toList()?

Imagine a scenario where we intend to apply filtering/mapping to a Sequence, and we desire these operations to be executed eagerly in one go as soon as an item is requested during iteration. However, we don’t need this to happen immediately. asIterable() comes in handy here – it provides an object adhering to the Iterable interface but without any elements (deferring execution). When we first invoke a map/filter extension function (on Iterable), the entire original Sequence is iterated and processed eagerly in one shot. Conversely, toList() triggers the complete iteration of the Sequence upfront, even before we can apply map/filter to process the items. I’m using deferred/immediate execution and lazy/eager evaluation as defined in this excellent post.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 `fun createCountriesSequence() = sequence {
    println("Before")
    yield("France")
    println("next city")
    yield("Portugal")
    println("next city")
    yield("Russia")
}

    var countriesSequence  = createCountriesSequence()
    // converting the sequence to list immediatelly iterates the sequence
    var countries = countriesSequence.toList()
    println("after converting to list")

    // Before
    // next city
    // next city
    // after converting to list

    println()

    // what's the use of calling .asIterable() rather than directly .toList()?
    // with .asIterable we obtain a deferred collection (nothing runs until we first need it)
    // but as the extension functions in Iterable are eager, first time we apply a map or whatever the whole iteration is performed 
 
    countriesSequence = createCountriesSequence()
    //converting to iterable does not perform any iteration yet
    var countriesIterable = countriesSequence.asIterable()
    println("after creating iterable")

    println("for loop iteration:") //no iteration had happenned until now
    for (country in countriesIterable) {
        println(country)
    }

    // after creating iterable
    // for loop iteration:
    // Before
    // France
    // next city
    // Portugal
    // next city
    // Russia
    
    println()

    countriesSequence  = createCountriesSequence()
    countriesIterable = countriesSequence.asIterable()
    println(countriesIterable::class)
    println("after creating iterable") //no iteration has happenned yet (deferred execution)

    //applying an Iterable extension function performs the whole iteration (eager evaluation)
    val upperCountries = countriesIterable.map({ 
        println("mapping: $it")
        it.uppercase() 
    })
    println("after mapping")
    println(upperCountries::class.simpleName)


    // after creating iterable
    // Before
    // mapping: France
    // next city
    // mapping: Portugal
    // next city
    // mapping: Russia
    // after mapping
    // ArrayList` 

One final observation: The iterator() method within Iterable or Sequence must be designated as an operator. Why? Documentation and discussions provide this explanation:

The for-loop relies on the iterator(), next(), and hasNext() methods to be marked with operator. This goes beyond operator overloading and constitutes another use case for the operator keyword.

The crux is that specific syntax leverages these functions without a visible syntactic call. Without the operator marking, this special syntax wouldn’t be accessible.

Keep in mind that Kotlin (similar to Python) doesn’t allow defining custom operators – we can only overload existing ones.

Licensed under CC BY-NC-SA 4.0
Last updated on Mar 08, 2024 05:27 +0100