Memproses Serangkaian Item dengan Iterators
Pola (pattern) iterator memungkinkan kita buat melakukan suatu tugas secara berurutan pada serangkaian item. Sebuah iterator bertanggung jawab atas logika untuk melakukan iterasi melewati setiap item dan menentukan kapan rangkaian tersebut sudah selesai. Saat kita memakai iterator, kita tidak perlu repot-repot mengimplementasikan ulang logika tersebut sendiri.
Di Rust, iterators itu lazy (malas), yang artinya mereka tidak punya efek
apa-apa sampai kita memanggil method-method yang bakal mengonsumsi (consume)
iterator itu untuk memakainya sampai habis. Sebagai contoh, kode di Listing
13-10 membuat sebuah iterator atas item-item di dalam vector v1 dengan
memanggil method iter yang didefinisikan pada Vec<T>. Kode ini kalau
berdiri sendiri tidak melakukan hal berguna apa pun.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
Iterator ini disimpan di dalam variabel v1_iter. Setelah kita membuat sebuah
iterator, kita bisa memakainya dalam berbagai cara. Di Listing 3-5, kita
beriterasi melewati sebuah array memakai for loop untuk mengeksekusi
beberapa kode pada masing-masing itemnya. Di balik layar, hal ini secara
implisit membuat lalu mengonsumsi sebuah iterator, tapi kita melewatkan detail
tentang gimana sebenarnya itu bekerja sampai saat ini.
Di contoh pada Listing 13-11, kita memisahkan proses pembuatan iterator dari
penggunaan iterator tersebut di dalam for loop. Saat for loop ini dipanggil
memakai iterator di v1_iter, setiap elemen di iterator tersebut dipakai
dalam satu putaran (iteration) loop, yang mana mencetak setiap nilainya.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
Di bahasa pemrograman yang tidak menyediakan iterator dari standard library-nya, kita kemungkinan bakal menulis fungsionalitas yang sama ini dengan memulai sebuah variabel di indeks 0, memakai variabel tersebut buat mengindeks ke dalam vector untuk mendapatkan nilai, lalu menambah nilai variabel itu di dalam loop sampai jumlahnya mencapai total item yang ada di vector.
Iterators menangani semua logika tersebut buat kita, mengurangi kode yang berulang-ulang (repetitive code) yang mana bisa saja kita bikin salah. Iterators ngasih kita fleksibilitas lebih buat memakai logika yang sama dengan berbagai jenis urutan data, bukan cuma struktur data yang bisa kita indeks aja, kayak vector. Mari kita teliti gimana cara iterators melakukan itu.
Trait Iterator dan Method next
Semua iterators mengimplementasikan sebuah trait bernama Iterator yang
didefinisikan di standard library. Definisi dari trait ini kelihatan kayak gini:
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods dengan implementasi default dihilangkan
}
}
Perhatikan bahwa definisi ini memakai beberapa sintaks baru: type Item dan
Self::Item, yang mendefinisikan sebuah associated type dengan trait ini.
Kita bakal membahas associated types lebih mendalam di Bab 20. Buat sekarang,
yang perlu kita tahu adalah bahwa kode ini bilang kalau buat mengimplementasikan
trait Iterator, kita juga harus mendefinisikan tipe Item, dan tipe Item
ini dipakai di tipe kembalian (return type) dari method next. Dengan kata
lain, tipe Item bakal jadi tipe yang dikembalikan dari iterator tersebut.
Trait Iterator cuma mewajibkan para peng-implementasi (implementors) buat
mendefinisikan satu method saja: yaitu method next, yang mengembalikan satu
item dari iterator pada satu waktu, dibungkus di dalam Some, dan, ketika
iterasi selesai, mengembalikan None.
Kita bisa memanggil method next pada iterators secara langsung; Listing 13-12
mendemonstrasikan nilai-nilai apa aja yang dikembalikan dari pemanggilan
berulang ke next pada iterator yang dibikin dari vector.
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
next pada sebuah iteratorPerhatikan bahwa kita harus membikin v1_iter jadi mutable: memanggil
method next pada sebuah iterator bakal mengubah state (keadaan) internal
yang dipakai sama iterator tersebut untuk melacak (keep track of) ada di mana
dia saat ini di urutan tersebut. Dengan kata lain, kode ini mengonsumsi
(consumes), atau menghabiskan, iterator-nya. Setiap pemanggilan ke next
bakal “memakan” satu item dari iterator-nya. Kita tidak perlu membikin
v1_iter jadi mutable saat kita memakai for loop karena loop tersebut
mengambil kepemilikan (ownership) dari v1_iter dan membikinnya mutable di
balik layar.
Perhatikan juga bahwa nilai yang kita dapat dari pemanggilan next adalah
referensi immutable ke nilai-nilai yang ada di vector-nya. Method iter
menghasilkan sebuah iterator yang berisi referensi immutable. Kalau kita
mau membuat iterator yang mengambil kepemilikan dari v1 lalu mengembalikan
nilai yang owned (dimiliki), kita bisa memanggil into_iter alih-alih iter.
Sama juga halnya, kalau kita mau beriterasi melewati referensi mutable, kita
bisa memanggil iter_mut alih-alih iter.
Method-method yang Mengonsumsi Iterator
Trait Iterator punya sejumlah method berbeda dengan implementasi default
yang disediakan oleh standard library; kita bisa tahu soal method-method
ini dengan melihat dokumentasi API standard library untuk trait Iterator.
Beberapa dari method ini memanggil method next di dalam definisi mereka,
dan inilah alasannya kenapa kita diwajibkan buat mengimplementasikan method
next saat kita mau mengimplementasikan trait Iterator.
Method-method yang memanggil next ini disebut consuming adapters, karena
memanggil mereka bakal menghabiskan iterator-nya. Salah satu contohnya adalah
method sum, yang mengambil kepemilikan dari iterator lalu beriterasi melewati
item-itemnya dengan memanggil next secara berulang, yang mana ini bakal
mengonsumsi iterator tersebut. Selama ia beriterasi, method ini menambahkan
tiap item ke dalam sebuah total berjalan (running total) lalu mengembalikan
totalnya saat iterasi selesai. Listing 13-13 punya contoh tes yang
mengilustrasikan pemakaian method sum.
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
sum buat mendapatkan total dari semua item di dalam iteratorKita tidak diizinkan buat memakai v1_iter lagi setelah memanggil sum karena
sum mengambil kepemilikan dari iterator tempat dia dipanggil.
Method-method yang Menghasilkan Iterator Lain
Iterator adapters adalah method-method yang didefinisikan pada trait Iterator
yang tidak mengonsumsi iterator-nya. Alih-alih mengonsumsi, mereka malah
menghasilkan iterator-iterator yang berbeda dengan cara mengubah beberapa aspek
dari iterator aslinya.
Listing 13-14 menunjukkan contoh dari pemanggilan method iterator adapter map,
yang menerima sebuah closure buat dipanggil pada setiap item saat item-item
tersebut dilewati dalam proses iterasi. Method map mengembalikan sebuah
iterator baru yang menghasilkan item-item yang sudah dimodifikasi tersebut.
Closure di sini membuat iterator baru di mana tiap item dari vector bakal
ditambahkan 1.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
map buat membuat iterator baruNamun, kode ini menghasilkan sebuah peringatan (warning):
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
Kode di Listing 13-14 tidak melakukan apa-apa; closure yang sudah kita tentukan itu tidak akan pernah dipanggil. Peringatannya mengingatkan kita soal alasannya kenapa: iterator adapters itu lazy (malas), dan kita harus mengonsumsi iterator-nya di sini.
Untuk membereskan peringatan ini dan mengonsumsi iterator-nya, kita bakal
memakai method collect, yang sudah kita pakai bersama env::args di Listing
12-1. Method ini mengonsumsi iterator-nya lalu mengumpulkan (collects) nilai-
nilai yang dihasilkan ke dalam sebuah tipe data koleksi.
Di Listing 13-15, kita mengumpulkan hasil dari iterasi iterator yang dikembalikan
oleh panggilan ke map menjadi sebuah vector. Vector ini nantinya bakal berisi
setiap item dari vector aslinya, masing-masing ditambah 1.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
}
map buat membuat iterator baru, lalu memanggil method collect buat mengonsumsi iterator baru itu dan membuat sebuah vectorKarena map menerima sebuah closure, kita bisa menentukan operasi apa pun yang
mau kita lakukan pada tiap itemnya. Ini adalah contoh yang bagus tentang gimana
closures memungkinkan kita buat mengkustomisasi beberapa perilaku tertentu
sambil menggunakan kembali perilaku iterasi yang disediakan sama trait Iterator.
Kita bisa menyambung (chain) beberapa panggilan ke berbagai iterator adapters buat melakukan tindakan yang rumit pakai cara yang tetap enak dibaca. Tapi karena semua iterator itu lazy, kita harus memanggil salah satu dari method consuming adapter buat mendapatkan hasil dari rentetan panggilan ke iterator adapters tersebut.
Menggunakan Closures yang Menangkap Lingkungannya
Banyak iterator adapters menerima closures sebagai argumen, dan biasanya closures yang bakal kita berikan sebagai argumen ke iterator adapters itu adalah closures yang bakal menangkap lingkungan (environment) mereka.
Untuk contoh ini, kita bakal memakai method filter yang menerima sebuah
closure. Closure ini mengambil item dari iterator lalu mengembalikan sebuah
bool. Kalau closure-nya mengembalikan true, nilainya bakal diikutsertakan
dalam iterasi yang dihasilkan oleh filter. Kalau closure-nya mengembalikan
false, nilainya tidak bakal diikutsertakan.
Di Listing 13-16, kita memakai filter bersama sebuah closure yang menangkap
variabel shoe_size dari lingkungannya untuk beriterasi melewati sekumpulan
instance struct Shoe. Ini cuma bakal mengembalikan sepatu-sepatu yang punya
ukuran sama dengan yang ditentukan.
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
filter bersama sebuah closure yang menangkap shoe_sizeFungsi shoes_in_size mengambil kepemilikan dari sebuah vector sepatu (shoes)
dan ukuran sepatu sebagai parameternya. Ia mengembalikan vector yang hanya
berisi sepatu-sepatu dengan ukuran yang ditentukan tersebut.
Di dalam body dari shoes_in_size, kita memanggil into_iter buat membuat
sebuah iterator yang mengambil kepemilikan dari vector-nya. Kemudian kita
memanggil filter buat mengadaptasi iterator itu menjadi iterator baru yang
hanya mengandung elemen-elemen yang bikin closure-nya mengembalikan true.
Closure-nya menangkap parameter shoe_size dari lingkungan di sekitarnya dan
membandingkan nilai itu dengan setiap ukuran dari sepatunya, mempertahankan
hanya sepatu-sepatu dengan ukuran yang sesuai. Terakhir, memanggil collect
bakal mengumpulkan (gathers) nilai-nilai yang dikembalikan oleh iterator yang
diadaptasi ini ke dalam sebuah vector yang kemudian dikembalikan oleh fungsi
tersebut.
Pengujian ini menunjukkan bahwa saat kita memanggil shoes_in_size, kita cuma
bakal dapat balik sepatu-sepatu yang punya ukuran yang sama dengan nilai yang
kita tentukan.