Sequence<T> adalah collection yang mengevaluasi elemennya secara lazy. Berbeda dengan List yang mengevaluasi semua operasi antara secara eager, Sequence hanya memproses elemen saat dibutuhkan oleh operasi terminal seperti toList() atau count(). Pada chapter ini kita akan memahami cara membuat Sequence, pipeline operasinya, dan bagaimana lazy evaluation menghindari alokasi list antara pada pipeline panjang.

Membuat Sequence #

Ada beberapa cara membuat Sequence:

CaraSumberKarakteristik
sequenceOf(...)Elemen literalSeperti listOf tetapi lazy
coll.asSequence()Collection apa punMembungkus collection sebagai sequence
generateSequence(seed) { next }Seed + fungsiBisa tak terbatas
sequence { yield(...) }Builder dengan lambdaPaling fleksibel, bisa yieldAll

Pipeline Lazy #

Operasi pada Sequence dibagi menjadi dua kelompok:

  • Intermediate (map, filter, onEach, sorted, distinct, take, drop): mengembalikan Sequence baru tanpa evaluasi.
  • Terminal (toList, count, forEach, sum, fold, first): memicu evaluasi dan menghasilkan nilai konkret.

Saat kita menulis seq.map { ... }.filter { ... }.take(3), tidak ada satu pun elemen yang diproses. Evaluasi baru terjadi ketika operasi terminal dipanggil.

generateSequence: Deret Tak Terbatas #

generateSequence(seed) { prev -> next } menghasilkan sequence yang bisa tak terbatas. Karena lazy, kita bisa take(n) tanpa khawatir memori habis:

kotlin Contoh: generateSequence
val duaPangkat = generateSequence(1) { it * 2 }
val ambil8 = duaPangkat.take(8).toList()
// [1, 2, 4, 8, 16, 32, 64, 128]

Tanpa take, sequence akan terus menghasilkan nilai saat diiterasi — kita wajib membatasi dengan take atau operasi terminal lain yang berhenti lebih awal.

Builder sequence dengan yield #

Cara paling fleksibel adalah builder sequence { ... }. Di dalam lambda, kita memanggil yield(value) untuk menghasilkan satu elemen atau yieldAll(iterable) untuk banyak elemen. Eksekusi dijeda dan dilanjutkan saat elemen diminta.

kotlin Contoh: builder sequence
val fibo = sequence {
    var a = 0; var b = 1
    while (true) {
        yield(a)
        val next = a + b
        a = b; b = next
    }
}
val fibo10 = fibo.take(10).toList()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Builder sequence di balik layar menggunakan coroutine. Lambda tidak berjalan sekaligus — yield menyerahkan kontrol kembali ke pemanggil dan melanjutkan saat elemen berikutnya diminta.

Contoh Lengkap #

kotlin
fun main() {
    val angka = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    // Eager: pipeline pada List, setiap tahap membuat list baru
    val eagerHasil = angka
        .map { it * it }
        .filter { it > 10 }
        .take(3)
    println("Eager: $eagerHasil")

    // Lazy: pipeline pada Sequence, evaluasi sekali jalan
    val lazyHasil = angka
        .asSequence()
        .map { it * it }
        .filter { it > 10 }
        .take(3)
        .toList()
    println("Lazy: $lazyHasil")

    // sequenceOf: membuat sequence langsung
    val seq = sequenceOf(1, 2, 3, 4, 5)
    println("sequenceOf: ${seq.toList()}")

    // generateSequence: dengan seed dan next function
    val duaKali = generateSequence(1) { it * 2 }
    val ambil = duaKali.take(8).toList()
    println("generateSequence 2x: $ambil")

    // sequence tak terbatas dengan kondisi berhenti
    val fibo = sequence {
        var a = 0; var b = 1
        while (true) {
            yield(a)
            val next = a + b
            a = b; b = next
        }
    }
    val fibo10 = fibo.take(10).toList()
    println("Fibonacci: $fibo10")

    // Debug dengan onEach: lihat elemen yang dilewatkan tiap tahap
    print("Trace lazy: ")
    angka.asSequence()
        .onEach { print("[$it]") }
        .map { it * it }
        .onEach { print("=$it ") }
        .filter { it > 10 }
        .take(2)
        .forEach { print("-> $it; ") }
    println()

    // Bandingkan dengan eager yang mencetak semua elemen per tahap
    print("Trace eager: ")
    angka
        .map { it * it }
        .also { println("setelah map: $it") }
        .filter { it > 10 }
        .also { println("setelah filter: $it") }
        .take(2)
        .also { println("setelah take: $it") }
}

Output yang Dihasilkan:

text
Eager: [16, 25, 36]
Lazy: [16, 25, 36]
sequenceOf: [1, 2, 3, 4, 5]
generateSequence 2x: [1, 2, 4, 8, 16, 32, 64, 128]
Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Trace lazy: [1]=1 [2]=4 [3]=9 [4]=16 -> 16; [5]=25 -> 25; 
Trace eager: setelah map: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
setelah filter: [16, 25, 36, 49, 64, 81, 100]
setelah take: [16, 25]

Mari kita bedah baris-baris penting di atas:

  1. angka.map { it * it }.filter { it > 10 }.take(3) (eager): setiap tahap membuat list baru. map mengubah 10 elemen menjadi 10 kuadrat, lalu filter menyaring 7 elemen, lalu take(3) mengambil 3. Total tiga list dialokasikan.
  2. angka.asSequence().map { ... }.filter { ... }.take(3).toList() (lazy): pipeline dijalankan elemen demi elemen. Saat take(3) meminta elemen ke-3, sequence baru memproses elemen yang diperlukan dan berhenti. Hanya satu list hasil akhir yang dialokasikan.
  3. generateSequence(1) { it * 2 }: seed 1, fungsi next it * 2. Sequence bisa berjalan tak terbatas. take(8) membatasi menjadi 8 elemen.
  4. sequence { ... yield(a) ... }: builder dengan lambda. yield(a) menghasilkan a lalu memberhentikan eksekusi. Saat elemen berikutnya diminta, eksekusi dilanjutkan setelah yield. Loop while (true) tidak mengakibatkan infinite loop karena yield memberhentikan sementara.
  5. onEach { print("[$it]") }: intermediate operation yang menjalankan lambda tiap elemen dilewatkan tanpa mengubah nilai. Berguna untuk debugging pipeline lazy.
  6. Trace lazy: output [1]=1 [2]=4 [3]=9 [4]=16 -> 16; [5]=25 -> 25; menunjukkan sequence memproses elemen 1, 2, 3, 4 untuk mendapatkan hasil pertama (16), lalu elemen 5 untuk hasil kedua (25). Setelah take(2) terpenuhi, iterasi berhenti — elemen 6 sampai 10 tidak diproses sama sekali.
  7. Trace eager: output menunjukkan semua elemen diproses pada setiap tahap. map menghasilkan list 10 elemen, filter menghasilkan list 7 elemen, take(2) menghasilkan list 2 elemen. Total 17 elemen list antara dibuat.

Kapan Memakai Sequence #

  • Pipeline panjang (banyak operasi intermediate berurutan).
  • Collection besar — alokasi list antara pada eager bisa boros memori.
  • take(n) awal — sequence berhenti begitu cukup, eager memproses semua elemen dulu.
  • Deret tak terbatas atau generatif (Fibonacci, prime, stream data).

Referensi Lanjutan #

Setelah memahami dasar sequence, kita akan membandingkan secara langsung kapan eager (List) dan kapan lazy (Sequence) menjadi pilihan yang lebih tepat.