Lewati ke konten

Iterator Kustom di Rust

Di halaman Iterator, kamu memakai iterator yang disediakan standard library: vec.iter(), .map(), .filter(), .sum(). Tapi kamu juga bisa membuat iterator sendiri. Hitung mundur, deret Fibonacci, halaman-halaman sebuah laporan, baris-baris dari format file buatanmu: apa saja yang memberikan nilai satu per satu bisa dijadikan iterator.

Kabar baiknya: kamu hanya perlu menulis satu method, yaitu next. Setelah itu Rust memberimu semua adapter (map, filter, take, sum, zip, dan puluhan lainnya) secara gratis. Bayangkan mesin nomor antrean. Kamu hanya membuat bagian yang mencetak nomor berikutnya. Antreannya, layarnya, dan tulisan “nomor yang sedang dilayani” semuanya otomatis bekerja.

Untuk menjadikan sebuah tipe iterator, implementasikan trait Iterator. Trait ini butuh dua hal:

  • type Item: jenis nilai yang diberikan iterator (lihat Trait lanjutan untuk penjelasan lebih lanjut tentang associated type).
  • fn next(&mut self) -> Option<Self::Item>: kembalikan Some(value) untuk nilai berikutnya, atau None kalau sudah tidak ada lagi.
struct Countdown {
current: u32,
}
impl Countdown {
fn new(from: u32) -> Self {
Countdown { current: from }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None // selesai
} else {
let value = self.current;
self.current -= 1;
Some(value)
}
}
}
fn main() {
let mut countdown = Countdown::new(3);
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
for n in Countdown::new(3) {
println!("{}...", n);
}
println!("Meluncur!");
}

Output:

Some(3)
Some(2)
Some(1)
None
None
3...
2...
1...
Meluncur!
▶ Coba sendiri

Beberapa hal yang perlu diperhatikan:

  • next menerima &mut self karena ia mengubah iterator. Struct-nya mengingat posisinya (current), dan setiap pemanggilan memajukannya. Karena itulah countdown harus let mut.
  • Mengembalikan None artinya “aku sudah selesai”. Perulangan for memanggil next terus-menerus dan berhenti di None yang pertama.
  • Hitung mundur kita terus mengembalikan None setelah selesai. Itu sopan, tapi tidak diwajibkan oleh trait-nya; ada iterator yang bisa mulai lagi. Kalau kamu butuh jaminan itu, panggil .fuse() pada iterator apa saja.

Iterator tidak harus berakhir. Deret Fibonacci (0, 1, 1, 2, 3, 5, 8, …) berlanjut selamanya: setiap angka adalah jumlah dari dua angka sebelumnya. Iterator-nya cukup mengingat dua angka terakhir:

struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.current;
// checked_add mengembalikan None kalau terjadi overflow, dan `?` meneruskan None itu,
// jadi iterator berhenti alih-alih crash saat angkanya terlalu besar.
let new_next = self.current.checked_add(self.next)?;
self.current = self.next;
self.next = new_next;
Some(value)
}
}
fn main() {
let first_ten: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", first_ten);
}

Output:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
▶ Coba sendiri

Kuncinya adalah .take(10). Tanpanya, collect() akan mencoba mengumpulkan seluruh deret. Untuk iterator tanpa akhir, selalu batasi dengan sesuatu seperti take, take_while, atau find.

Inilah bagian terbaiknya. Trait Iterator punya sekitar 75 method, dan semuanya kecuali next sudah punya implementasi bawaan yang dibangun di atas next. Jadi begitu kamu menulis next, tipemu bisa memakai semuanya:

struct Countdown {
current: u32,
}
impl Countdown {
fn new(from: u32) -> Self {
Countdown { current: from }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None
} else {
let value = self.current;
self.current -= 1;
Some(value)
}
}
}
fn main() {
let total: u32 = Countdown::new(5).sum();
println!("Jumlah: {}", total);
let evens: Vec<u32> = Countdown::new(10).filter(|n| n % 2 == 0).collect();
println!("Genap: {:?}", evens);
let labels: Vec<String> = Countdown::new(3).map(|n| format!("T-{}", n)).collect();
println!("Label: {:?}", labels);
let multiple = Countdown::new(100).find(|n| n % 7 == 0);
println!("Kelipatan 7 pertama: {:?}", multiple);
let pairs: Vec<(u32, char)> = Countdown::new(3).zip(['a', 'b', 'c']).collect();
println!("Dipasangkan: {:?}", pairs);
}

Output:

Jumlah: 15
Genap: [10, 8, 6, 4, 2]
Label: ["T-3", "T-2", "T-1"]
Kelipatan 7 pertama: Some(98)
Dipasangkan: [(3, 'a'), (2, 'b'), (1, 'c')]
▶ Coba sendiri

Ini contoh bagus kenapa trait di Rust sangat berguna. Cukup satu method kecil, dan kamu mendapat satu kotak perkakas yang besar.

Adapter seperti map dan take tidak melakukan apa-apa dengan sendirinya. Mereka hanya menyusun sebuah “rencana”. Pekerjaan baru dijalankan saat ada yang mengonsumsi iterator itu: perulangan for, collect, sum, dan sebagainya. Istilahnya, iterator itu lazy (malas). Mari kita buktikan dengan iterator yang mencetak sesuatu setiap kali next dipanggil:

struct Noisy {
n: u32,
}
impl Iterator for Noisy {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.n += 1;
println!(" next() dipanggil, memberi {}", self.n);
Some(self.n)
}
}
fn main() {
println!("Menyusun rangkaian...");
let chain = Noisy { n: 0 }.map(|x| x * 10).take(3);
println!("Belum ada yang berjalan. Sekarang dikonsumsi:");
for value in chain {
println!("dapat {}", value);
}
println!("Selesai");
}

Output:

Menyusun rangkaian...
Belum ada yang berjalan. Sekarang dikonsumsi:
next() dipanggil, memberi 1
dapat 10
next() dipanggil, memberi 2
dapat 20
next() dipanggil, memberi 3
dapat 30
Selesai
▶ Coba sendiri

Ada dua pelajaran di sini. Pertama, menyusun rangkaian tidak mencetak apa pun, sama sekali tidak ada pemanggilan next(). Kedua, nilai mengalir satu per satu: satu next, satu map, satu dapat, lalu nilai berikutnya. Tidak ada daftar tersembunyi di antaranya. Dan meski Noisy tidak pernah berakhir, program tetap selesai: setelah tiga item, take(3) berhenti meminta.

Membuat koleksimu bisa dipakai di perulangan for

Section titled “Membuat koleksimu bisa dipakai di perulangan for”

Sejauh ini, struct kita adalah iterator. Tapi sering kali kamu punya sebuah koleksi, yaitu struct yang menyimpan item, dan kamu ingin orang bisa melakukan perulangan atasnya, seperti pada Vec. Untuk itu, kamu mengimplementasikan IntoIterator. Inilah trait yang dipakai perulangan for di balik layar: for x in thing sebenarnya memanggil thing.into_iter().

Koleksi biasanya menyediakan tiga cara perulangan, dan akan lebih jelas kalau kamu melihatnya di tipe buatanmu sendiri:

Yang kamu tulis Method di baliknya Setiap item berupa Koleksinya setelah itu
for s in &playlist playlist.iter() &String (hanya baca) masih bisa dipakai
for s in &mut playlist playlist.iter_mut() &mut String (bisa diubah) masih bisa dipakai
for s in playlist playlist.into_iter() String (dimiliki) dipindahkan (move), hilang

Berikut Playlist yang mendukung ketiganya. Struct kita menyimpan lagu-lagunya di dalam Vec, jadi kita bisa memberikan iterator milik Vec itu sendiri, tanpa menulis iterator baru:

struct Playlist {
songs: Vec<String>,
}
impl Playlist {
fn iter(&self) -> std::slice::Iter<'_, String> {
self.songs.iter()
}
fn iter_mut(&mut self) -> std::slice::IterMut<'_, String> {
self.songs.iter_mut()
}
}
// Membuat `for song in &playlist` bisa dipakai
impl<'a> IntoIterator for &'a Playlist {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
// Membuat `for song in &mut playlist` bisa dipakai
impl<'a> IntoIterator for &'a mut Playlist {
type Item = &'a mut String;
type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
// Membuat `for song in playlist` bisa dipakai (dan menghabiskan playlist-nya)
impl IntoIterator for Playlist {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.songs.into_iter()
}
}
fn main() {
let mut playlist = Playlist {
songs: vec![String::from("intro"), String::from("main theme"), String::from("outro")],
};
for song in &playlist {
println!("Membaca: {}", song);
}
for song in &mut playlist {
*song = song.to_uppercase();
}
println!("Terpanjang: {:?}", playlist.iter().max_by_key(|s| s.len()));
for song in playlist {
println!("Mengambil: {}", song);
}
// `playlist` sudah dipindahkan oleh perulangan terakhir, jadi tidak bisa dipakai lagi di sini.
}

Output:

Membaca: intro
Membaca: main theme
Membaca: outro
Terpanjang: Some("MAIN THEME")
Mengambil: INTRO
Mengambil: MAIN THEME
Mengambil: OUTRO
▶ Coba sendiri

Judul lagunya sengaja dibiarkan dalam bahasa Inggris, karena baris Terpanjang bergantung pada panjang setiap judul.

Baris kodenya memang banyak, tapi setiap impl bentuknya sama: sebutkan Item-nya apa, sebutkan tipe iterator yang dikembalikan (IntoIter), lalu kembalikan iterator itu. 'a adalah sebuah lifetime (masa hidup): artinya “item yang dipinjam hidup selama playlist yang dipinjam masih hidup”. Kamu bisa menyalin pola ini untuk hampir semua koleksi yang membungkus Vec.

Ada dua trait tambahan yang layak kamu kenal. Kamu tidak sering membutuhkannya, tapi adapter seperti .rev() dan .len() bergantung pada keduanya:

  • DoubleEndedIterator menambahkan next_back, yang mengambil item dari ujung belakang. Trait ini membuka .rev().
  • ExactSizeIterator menjamin iterator tahu persis berapa item yang tersisa. Trait ini membuka .len(). Kamu juga perlu memberi size_hint yang akurat di Iterator.
// Menghasilkan low, low+1, ..., high-1 (seperti range low..high)
struct Between {
low: u32,
high: u32,
}
impl Iterator for Between {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.low >= self.high {
return None;
}
let value = self.low;
self.low += 1;
Some(value)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.high.saturating_sub(self.low) as usize;
(left, Some(left))
}
}
impl DoubleEndedIterator for Between {
fn next_back(&mut self) -> Option<u32> {
if self.low >= self.high {
return None;
}
self.high -= 1;
Some(self.high)
}
}
impl ExactSizeIterator for Between {}
fn main() {
let backwards: Vec<u32> = Between { low: 1, high: 6 }.rev().collect();
println!("{:?}", backwards);
let mut both_ends = Between { low: 1, high: 6 };
println!("depan {:?}, belakang {:?}", both_ends.next(), both_ends.next_back());
println!("sisa {}", both_ends.len());
}

Output:

[5, 4, 3, 2, 1]
depan Some(1), belakang Some(5)
sisa 3
▶ Coba sendiri

Kedua ujungnya bergerak saling mendekat, dan iterator selesai saat keduanya bertemu. impl ExactSizeIterator for Between {} memang kosong: len() memakai size_hint yang sudah kamu tulis.

Menulis struct dan impl sangat cocok untuk iterator yang dipakai berulang kali. Untuk keperluan sekali pakai, standard library punya dua helper di std::iter:

  • iter::from_fn(closure): memanggil closure milikmu setiap kali butuh nilai. Closure-nya mengembalikan Some(value) atau None, persis seperti next.
  • iter::successors(first, closure): dimulai dari sebuah nilai awal, lalu closure menghitung setiap nilai berikutnya dari nilai sebelumnya. Mengembalikan None akan mengakhirinya.
use std::iter;
fn main() {
// from_fn: closure-nya menyimpan state-nya sendiri (count)
let mut count = 3;
let countdown: Vec<u32> = iter::from_fn(|| {
if count == 0 {
None
} else {
count -= 1;
Some(count + 1)
}
})
.collect();
println!("{:?}", countdown);
// successors: setiap nilai dihitung dari nilai sebelumnya
let powers: Vec<u32> = iter::successors(Some(1u32), |&n| n.checked_mul(10)).collect();
println!("{:?}", powers);
// Fibonacci dalam satu baris: bawa dua angka terakhir sebagai pasangan
let fib: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b)))
.map(|(a, _)| a)
.take(10)
.collect();
println!("{:?}", fib);
}

Output:

[3, 2, 1]
[1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
▶ Coba sendiri

powers berhenti dengan sendirinya: pangkat sepuluh berikutnya, 10.000.000.000, tidak muat di u32, jadi checked_mul mengembalikan None dan iterator-nya berakhir. Aturan yang bagus: pakai jalan pintas ini di dalam satu fungsi, dan tulis struct bernama kalau kode lain perlu membuat iterator itu, atau kalau state-nya banyak.

Lupa mut saat memanggil next sendiri. next mengubah iterator, jadi variabelnya harus mutable:

let countdown = Countdown { current: 3 };
println!("{:?}", countdown.next());
error[E0596]: cannot borrow `countdown` as mutable, as it is not declared as mutable
--> src/main.rs:20:22
|
20 | println!("{:?}", countdown.next());
| ^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
19 | let mut countdown = Countdown { current: 3 };
| +++

Compiler bilang countdown tidak bisa dipinjam sebagai mutable karena tidak dideklarasikan dengan mut, lalu menunjukkan tempat untuk menambahkannya. Tambahkan mut. (Perulangan for tidak membutuhkannya, karena ia mengambil iterator-nya dan mengurus hal itu untukmu.)

Lupa type Item. Trait-nya perlu tahu apa yang kamu berikan:

error[E0046]: not all trait items implemented, missing: `Item`
--> src/main.rs:5:1
|
5 | impl Iterator for Countdown {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation
|
= help: implement the missing item: `type Item = /* Type */;`

Compiler bilang belum semua item trait diimplementasikan: Item tidak ada. Tambahkan type Item = u32; (atau tipe apa pun yang dihasilkan iterator-mu) di dalam impl.

Mengumpulkan iterator tanpa akhir. Fibonacci::new().collect::<Vec<_>>() (tanpa take) bisa dikompilasi, tapi tidak akan pernah selesai dengan cara yang berguna: ia berjalan sampai angkanya overflow, atau selamanya kalau next milikmu tidak pernah mengembalikan None. Compiler tidak bisa mendeteksi ini. Kalau sebuah iterator mungkin tidak berakhir, selalu tambahkan take, take_while, atau find sebelum mengonsumsinya.

  • Implementasikan Iterator dengan type Item dan fn next(&mut self) -> Option<Self::Item>. Kembalikan None saat sudah selesai.
  • Begitu next ada, kamu mendapat map, filter, take, sum, zip, dan puluhan method lain secara gratis.
  • Iterator itu lazy: tidak ada yang terjadi sampai perulangan for, collect, sum, dan sebagainya mengonsumsinya, dan item mengalir satu per satu.
  • Implementasikan IntoIterator untuk &MyType, &mut MyType, dan MyType supaya perulangan for bisa meminjam, meminjam secara mutable, atau mengonsumsi.
  • DoubleEndedIterator memungkinkan .rev(); ExactSizeIterator memungkinkan .len().
  • Untuk iterator sekali pakai yang cepat, pakai std::iter::from_fn atau std::iter::successors.

Kuis singkat

1. Apa yang harus dikembalikan next saat iterator sudah tidak punya item lagi?

2. Setelah hanya mengimplementasikan next, bisakah kamu memanggil .map() dan .sum() pada iterator-mu?

3. Pada contoh evaluasi lazy, kapan next() pertama kali dipanggil?

4. Impl mana yang membuat for song in &playlist bisa dipakai?

5. Trait mana yang kamu butuhkan supaya .rev() bisa dipakai pada iterator-mu?

Skor: 0 / 5