Utku's answer covers the why (yeah, the sequence
code is much less optimal here) but generally speaking - Sequence
s are meant to be a complement to Iterable
s. They use the same functions, and you can chain them into processing pipelines.
The difference is that sequences are executed lazily, each item passing through the full pipeline before the next is handled, and items are only processed when they need to be.
For example, check this extremely good and plausible code:
import kotlin.system.measureTimeMillis
fun isMagicNumber(num: Double) = num == 10000.0
fun main(args: Array<String>) {
val numberStrings = (1..25000).map(Int::toString)
val itertime = measureTimeMillis {
numberStrings
.map(String::toInt).map { it * 2.0 }.first(::isMagicNumber)
}
println("Iterable version: $itertime ms")
val seqtime = measureTimeMillis {
numberStrings.asSequence()
.map(String::toInt).map { it * 2.0 }.first(::isMagicNumber)
}
print("Sequence version: $seqtime ms")
}
Starting with a list of 25,000 numbers, as Strings, the pipeline is
- map to an
Int
- double it (converting to a
Double
)
- get the first one that meets the condition (equals 10,000, in this case)
The Iterable
version does each step for the entire list, before moving onto the next step:
List<String> -> List<Int> -> List<Double> -> find element
It creates a new list each time (taking up memory), and it doesn't even start checking elements until it's made three lists and can start going over the last one - that's the earliest it can return a result.
Sequences do this:
String -> Int -> Double -> check
for each item. As soon as it hits the item that meets the check condition, it's done. That's gotta be way faster right!!!
Iterable version: 41 ms
Sequence version: 111 ms
Ah! Well. Nevertheless,
Turns out sequences have overhead (which is why a really basic for
loop will obliterate them if you can write one), and also computers are pretty darn good at iterating over things - there are lots of optimisations under the hood, creating new arrays and iterating over them can be faster than using linked lists, etc. Which is why you really need to profile what you're doing, if efficiency is what you're going for.
What if the source list (and therefore all the additional lists the Iterable
version creates) was 10x bigger, 250,000 items?
Iterable version: 260 ms
Sequence version: 155 ms
Oh hello, now we're getting somewhere. Turns out all that overhead starts to pile up after a while, and being able to exit early becomes important, and sequences start to get more efficient.
And this is just measuring time - you need to look at memory use too. Building huge lists can quickly use a lot of memory, or even become unrunnable (if you're doing Project Euler-levels of "make this work for a bajiliion kazillion items"). Sequences and their one-thing-at-a-time approach can keep the whole thing workable, and completable before the heat death of the universe
Also sequences can be infinite! Which limits where you can use them, or how efficient some operations are (last()
needs to run through the entire sequence), but it can also be used in situations where a fixed-size collection won't work.
So yeah, use the right tool for the job, and make sure that if efficiency is important, you're actually using the version that gives the best results. But sometimes readability and composability are more important, and being able to chain operations to do a thing is better than a mean and lean for
loop