Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Listing 13-10: Membuat sebuah iterator

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.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 13-11: Memakai iterator di dalam sebuah for loop

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.

Filename: src/lib.rs
#[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);
    }
}
Listing 13-12: Memanggil method next pada sebuah iterator

Perhatikan 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.

Filename: src/lib.rs
#[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);
    }
}
Listing 13-13: Memanggil method sum buat mendapatkan total dari semua item di dalam iterator

Kita 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.

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
Listing 13-14: Memanggil iterator adapter map buat membuat iterator baru

Namun, 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.

Filename: src/main.rs
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]);
}
Listing 13-15: Memanggil method map buat membuat iterator baru, lalu memanggil method collect buat mengonsumsi iterator baru itu dan membuat sebuah vector

Karena 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.

Filename: src/lib.rs
#[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")
                },
            ]
        );
    }
}
Listing 13-16: Memakai method filter bersama sebuah closure yang menangkap shoe_size

Fungsi 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.