Kotlin Collections – Sequences

Using Sequences – the equivalent to Java Streams.

// When chaining a e.g. filter and map together there will be intermediate Collections formed in the background. What if there are a large number of chained actions or the Collection is very large (or unknown size)?
// To avoid the creation of intermediate Collections we can use Sequences. Equivalent to Streams in Java, and aren't available on all platforms.
// Each element is evaluated and passed to next step in chain (if appropriate). No need for intermediates
// Only one function - iterator. Can convert a Collection to a Sequence but ONLY DO THIS FOR LARGE COLLECTIONS as Kotlin Collections are very efficient.
 fun main() {
    val immutableMap = mapOf(1 to Car3("green", "Toyota", 2015),
        2 to Car3("red", "Ford", 2016),
        3 to Car3("silver", "Honda", 2013),
        17 to Car3("red", "BMW", 2015),
        8 to Car3("green", "Ford", 2010)
    )

    // Two types of Sequence - intermediate (returns a Sequence to pass onto next operation) and terminal (terminates the chain)
    println(immutableMap.asSequence().filter { it.value.model == "Ford" }
        .map {it.value.colour}) // Intermediate - lazy i.e. not evaluated immediately. If you never reach terminal operation would be wasteful to evaluate intermediates

    listOf("Joe", "Mary", "Jane").asSequence()
        .filter { println("Filtering $it"); it[0] == 'J' } // Intermediate operation
        .map { println("Mapping $it"); it.toUpperCase() } // Intermediate operation. No return - need terminal operation

    val name = listOf("Joe", "Mary", "Jane").asSequence()
        .filter { println("Filtering $it"); it[0] == 'J' }
        .map { println("Mapping $it"); it.toUpperCase() }
        .toList() // Terminal operation - can now see results of operations
    println(name)

    // In some cases the desired result will be returned long before iterating the entire dataset, so many intermediate Collections could be avoided by using Sequences:
    println("======")
    val name1 = listOf("Joe", "Mary", "Jane").asSequence()
        .filter { println("Filtering $it"); it[0] == 'J' }
        .map { println("Mapping $it"); it.toUpperCase() }
        .find { it.endsWith('E') } // Operation completes without ever having to evaluate Mary and Jane
    println(name1)
    println("======")

    // With Collections, the order in which you call functions makes a difference. If we reverse the filter and map functions:
    val name2 = listOf("Joe", "Mary", "Jane")
        .map { println("Mapping $it"); it.toUpperCase() }
        .filter { println("Filtering $it"); it[0] == 'J' }
        .find { it.endsWith('E') } // All 3 names are mapped and then filtered - less efficient
    println(name2)
    println("======")

    // But with Sequences, the order in which you call functions may or may not make a difference. If we reverse the filter and map functions:
    val name3 = listOf("Joe", "Mary", "Jane").asSequence()
        .map { println("Mapping $it"); it.toUpperCase() }
        .filter { println("Filtering $it"); it[0] == 'J' }
        .find { it.endsWith('E') } // No difference
    println(name3)
    println("======")

    // But if we put Joe last:
    val name4 = listOf("Mary", "Jane", "Joe").asSequence()
        .map { println("Mapping $it"); it.toUpperCase() }
        .filter { println("Filtering $it"); it[0] == 'J' }
        .find { it.endsWith('E') } // Much less efficient
    println(name4)

    // Moral of the story: think out the order in which you run operations
}