Sepanjang section ini kita menjadi konsumen setia iterator: memakai iter(), merangkai map, menyaring dengan filter, lalu menagih hasil lewat collect. Sekarang saatnya membuka dapurnya. Semua method tersebut sesungguhnya hanya berdiri di atas satu method next — yang berarti tipe buatan kita sendiri dapat masuk ke seluruh ekosistem iterator cukup dengan mengimplementasikan trait Iterator. Begitu kontrak mini itu terpenuhi, for, map, filter, zip, enumerate, hingga sum dan collect tersedia gratis — tanpa satu baris pun kode tambahan dari kita. Pada chapter penutup ini kita bangun iterator klasik dari komunitas Rust bernama Counter, lalu membuktikan bahwa iterator buatan sendiri masuk mulus ke pipeline paling rumit sekalipun.

Rangkaian yang Tidak Tinggal di Koleksi #

Mari kita mulai dari masalah yang tidak bisa dijawab Vec. Program poin keanggotaan toko kita mengenal lima tingkat: nilai poin tiap tingkat adalah kelipatan sepuluh — 10, 20, 30, 40, 50. Nilai berpola seperti ini bisa dihitung bertahap; nilai tersebut tidak harus tinggal di koleksi mana pun sebelum benar-benar dibutuhkan. Kebutuhan semacam ini — “sumber nilai yang tahu cara menghasilkan elemen berikutnya” — adalah wilayah iterator, dan kita akan menyediakannya lewat struct Counter yang menyimpan satu field: posisi hitungan saat ini.

Implementasi Trait Iterator untuk Counter #

Berikut definisi lengkap Counter beserta implementasi trait-nya:

rust
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.count < 5 {
            self.count = self.count + 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter::new();

    println!("Hasil ke-1: {:?}", counter.next());
    println!("Hasil ke-2: {:?}", counter.next());
    println!("Hasil ke-3: {:?}", counter.next());
}

Output yang Dihasilkan:

text
Hasil ke-1: Some(1)
Hasil ke-2: Some(2)
Hasil ke-3: Some(3)

Mari kita bedah baris demi baris:

  1. Baris 1-3 (struct Counter { count: u32 }): struct hanya menyimpan satu field bernama count — kondisi internal iterator. Tidak ada Vec, tidak ada daftar elemen yang disimpan; elemen akan dihitung saat diminta.
  2. Baris 5-9 (impl Counter { fn new() ... }): constructor new() menyiapkan counter di posisi nol. Kebiasaan menyediakan constructor membuat pembuatan iterator ringkas dibanding menulis Counter { count: 0 } di setiap tempat.
  3. Baris 11 (impl Iterator for Counter {): sintaks implementasi trait persis seperti yang kita latih di section Traits — impl NamaTrait for NamaTipe.
  4. Baris 12 (type Item = u32;): associated type Item kita isi u32 — pernyataan resmi bahwa Counter menghasilkan angka. Kompilator mulai dari sini mengerti seluruh method bawaan Iterator yang bergantung pada Item.
  5. Baris 14-21 (fn next(&mut self) -> Option<u32>): satu-satunya method wajib. Logikanya: selama count masih di bawah 5, naikkan satu lalu kembalikan Some(self.count); begitu batas tercapai, kembalikan None selamanya. Perhatikan dua keputusan penting di dalamnya — self.count dinaikkan sebelum dikembalikan sehingga nilai pertama adalah 1 bukan 0, dan field count diubah lewat &mut self persis seperti yang dituntut tanda tangan next.
  6. Baris 24-29 (let mut counter = Counter::new(); ...): pengujian manual dengan tiga panggilan next membuktikan perilakunya: Some(1), Some(2), Some(3) — persis pola dari chapter pertama section ini, kini dari tipe buatan kita sendiri.

Counter dalam Perulangan for #

Pengujian berikutnya membuktikan klaim pertama dari gratisnya trait ini — for langsung menerima Counter (definisi struct dan kedua blok impl di atas tetap dipakai; salin bersama snippet berikut untuk menjalankannya):

rust
fn main() {
    for step in Counter::new() {
        println!("Langkah ke-{}", step);
    }
}

Output yang Dihasilkan:

text
Langkah ke-1
Langkah ke-2
Langkah ke-3
Langkah ke-4
Langkah ke-5

Uraian baris-per-baris:

  1. Baris 2 (for step in Counter::new()): perulangan for menuntut sumbernya mengimplementasikan trait IntoIterator — dan pustaka standar memberlakukan aturan bahwa setiap tipe Iterator otomatis memenuhi syarat tersebut. Konsekuensinya, Counter masuk ke for tanpa kita menulis IntoIterator sedikit pun.
  2. Baris 3 (println!("Langkah ke-{}", step);): badan perulangan berjalan lima kali. Pada panggilan next keenam, Counter mengembalikan None dan for berhenti — mekanisme driver yang sudah kita bedah di chapter pertama, kini menghidupkan tipe buatan sendiri.

Counter di Pipeline Lengkap #

Ujian sesungguhnya adalah pipeline bertingkat. Kita rangkai dua Counter dengan zip, kalikan setiap pasangan lewat map, saring hasil yang habis dibagi tiga lewat filter, lalu jumlahkan dengan sum:

rust
fn main() {
    let sum: u32 = Counter::new()
        .zip(Counter::new().skip(1))
        .map(|(a, b)| a * b)
        .filter(|x| x % 3 == 0)
        .sum();

    println!("Jumlah hasil kali yang habis dibagi 3: {}", sum);
}

Output yang Dihasilkan:

text
Jumlah hasil kali yang habis dibagi 3: 18

Mari kita lacak nilai elemen tahap demi tahap, karena di sinilah pembelajaran sebenarnya terjadi:

Tahap pipelineNilai yang mengalir
Counter::new()1, 2, 3, 4, 5
zip(Counter::new().skip(1))(1, 2), (2, 3), (3, 4), (4, 5)
map (perkalian tiap pasangan)2, 6, 12, 20
filter (habis dibagi 3)6, 12
sum18

Uraian baris-per-baris mengikuti tabel di atas:

  1. Baris 3 (.zip(Counter::new().skip(1))): Counter pertama menghasilkan 1 sampai 5. Counter kedua juga mulai dari 1, tetapi skip(1) membuang nilai pertamanya sehingga alirannya 2 sampai 5 — penempatan skip setelah Counter::new() mengubah titik mulai pasangan. zip lalu menjodohkan keduanya berdampingan dan berhenti ketika sisi yang lebih pendek habis: empat pasangan.
  2. Baris 4 (.map(|(a, b)| a * b)): closure menerima tuple hasil zip yang langsung dipecah pola (a, b). Perkalian menghasilkan 2, 6, 12, 20 — perhatikan bahwa elemen di sini sudah bukan keluaran Counter mentah; tipe mengalir dan berubah melewati adapter, dan Item = u32 yang kita tetapkan tetap menjadi titik awal yang sah.
  3. Baris 5 (.filter(|x| x % 3 == 0)): predicate membuang 2 dan 20, menyisakan 6 dan 12. Kedua angka ini lolos karena habis dibagi tiga.
  4. Baris 6 (.sum()): consumer penutup menjumlahkan seluruh aliran menjadi 18 — satu angka yang dihasilkan dari dua Counter, empat adapter, dan nol koleksi perantara.

Menghasilkan Koleksi dari Counter #

Penutup bukti: collect juga bekerja normal, sehingga Counter dapat diubah menjadi koleksi ketika hasil hitungannya benar-benar diperlukan sebagai data yang disimpan:

rust
fn main() {
    let steps: Vec<u32> = Counter::new().map(|value| value * 10).collect();

    println!("Nilai bertingkat: {:?}", steps);
}

Output yang Dihasilkan:

text
Nilai bertingkat: [10, 20, 30, 40, 50]

Uraian baris-per-baris:

  1. Baris 2 (Counter::new().map(|value| value * 10).collect()): aliran 1 sampai 5 dikalikan sepuluh per elemen, lalu collect menumpuk hasilnya menjadi Vec<u32> — anotasi : Vec<u32> memberi arah tujuan persis seperti chapter Consumer. Lima nilai poin keanggotaan kini menjadi data koleksi yang bisa dipass ke fungsi lain.
  2. Baris 4 (println!(...)): kelima nilai tampil sebagai Vec biasa — tidak ada yang membedakan asal-usulnya dari iterator buatan sendiri.

Ringkasan #

Mengimplementasikan trait Iterator untuk tipe buatan hanya menuntut dua hal: menetapkan type Item dan menulis next(&mut self) -> Option<Self::Item> lengkap dengan logika kapan mengembalikan Some dan kapan None. Counter klasik membuktikan seluruh janji kontrak ini: langsung diterima for lewat jembatan IntoIterator, menjalani pipeline zip, skip, map, filter hingga sum menghasilkan 18 tanpa kode tambahan, dan berubah menjadi Vec lewat collect. Dengan ini siklus section Iterator tertutup penuh: kita telah memahami kontraknya, tiga wajah pinjamannya, sifat lazynya, para adapter dan consumer-nya, hingga kemampuan membangunnya sendiri — bekal yang membuat pemrosesan data di Rust terasa deklaratif, aman, dan hemat sekaligus.