Closures

Closures seperti bentuk pendek dari function yang tidak diberikan nama. Terkadang mereka juga disebut sebagai lambda. Closures sangat mudah ditemukan karena mereka menggunakan || daripada menggunakan (). Closure sangat umum digunakan di Rust, dan sekali Anda belajar untuk menggunakannya, Anda akan merasa ketergantungan untuk terus menggunakannya. :D

Anda bisa mengikat (bind) closure ke sebuah variable, dan kemudian saat Anda menggunakannya, ia terlihat persis seperti fungsi pada umumnya:

fn main() {
    let my_closure = || println!("This is a closure");
    my_closure();
}

Jadi, closure yang di atas tidak mengambil apapun: || dan mencetak pesan: This is a closure.

Di antara || kita bisa menambahkan variabel input dan typenya, seperti yang kita tuliskan di antara () saat menuliskan sebuah function:

fn main() {
    let my_closure = |x: i32| println!("{}", x);

    my_closure(5);
    my_closure(5+5);
}

Hasil cetaknya adalah:

5
10

Di saat closure menjadi lebih rumit, Anda bisa menambahkan code block. Maka ia bisa dituliskan sepanjang yang Anda inginkan.

fn main() {
    let my_closure = || {
        let number = 7;
        let other_number = 10;
        println!("The two numbers are {} and {}.", number, other_number);
          // Closure ini bisa dibuat sepanjang yang Anda inginkan, sama seperti function.
    };

    my_closure();
}

Tapi closure sangat spesial karena mereka bisa mengambil variabel yang berada di luar closure bahkan jika Anda hanya menuliskan ||. Sehingga Anda bisa melakukan hal berikut ini:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = || println!("{}", number_one + number_two);
    my_closure();
}

Program di atas mencetak 16. Anda tidak perlu untuk menaruh apapun di antara || karena ia hanya mengambil number_one dan number_two dan menjumlahkannya.

Dari situlah nama closure berasal, karena ia mengambil variabel dan "enclose" (menyertakan) variabel tersebut di dalamny. Dan jika Anda menginginkan penjelahan yang lebih benar:

  • || yang tidak menyertakan variable dari luar adalah "anonymous function". Anonymous artinya "tidak memiliki nama". Ia bekerja mirip seperti function pada umumnya. Inilah yang biasanya disebut sebagai Lambda.
  • || yang menyertakan variabel dari luar adalah "closure". Ia "encloses"/"menyertakan" variabel di sekitarnya untuk digunakan.

Tapi banyak orang biasa menyebut fungsi yang ditulis dengan || sebagai closure, jadi Anda tidak perlu khawatir apapun namanya. Kita akan menyebutnya sebagai "closure" untuk semua yang ditulis menggunakan ||, tapi harus diingat bahwa itu juga bisa berarti adalah "anonymous function"/lambda.

Mengapa ada baiknya untuk mengetahui keduanya? Karena sebuah anonymous function sebenarnya membuat machine code yang sama sebagaimana function yang memiliki nama. Anonymous function terdengar seperti bahasa yang sangat tinggi, sehingga terkadang banyak orang berpikir bahwa machine codenya pastilah sangat rumit. Tetapi machine code yang dibuat Rust sama cepatnya seperti fungsi biasa.

Mari kita lihat beberapa hal lagi yang bisa dilakukan oleh closure. Anda juga bisa melakukan ini:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = |x: i32| println!("{}", number_one + number_two + x);
    my_closure(5);
}

Closure pada contoh di atas mengambil number_one dan number_two. Kita juga memberikannya variabel x dan mengatakan bahwa x adaalh 5. Kemudian menjumlahkan semua variabel tersebut untuk mencetak 21.

Biasanya, Anda melihat closure di Rust di dalam method, karena closure memanglah sangat nyaman untuk digunakan. Kita melihat closure (di chapter sebelumnya) dengan .map() dan .for_each(). Di bagian dimana kita menuliskan |x| untuk memasukkan element berikutnya ke dalam iterator, dan itu adalah closure.

Ini adalah contoh lainnya: method unwrap_or yang telah kita ketahui sebelumnya, bisa Anda gunakan untuk meberikan value jika unwrap tidak berfungsi. Sebelumya, kita menuliskan: let fourth = my_vec.get(3).unwrap_or(&0);. Tapi ada juga method unwrap_or_else yang memiliki closure di dalamnya. Sehingga Anda bisa menuliskannya seperti berikut:

fn main() {
    let my_vec = vec![8, 9, 10];

    let fourth = my_vec.get(3).unwrap_or_else(|| { // mencoba untuk melakukan unwrap. Jika ia tidak berhasil,
        if my_vec.get(0).is_some() {               // periksa apakah my_vec memiliki sesuatu pada index [0]
            &my_vec[0]                             // Berikan angka yang berada pada index ke-0 jika elementnya memang ada
        } else {
            &0 // jika tidak ada, berikan &0
        }
    });

    println!("{}", fourth);
}

Tentu saja, closure bisa ditulis dengan sangat simple. Contohnya, Anda bisa menulis let fourth = my_vec.get(3).unwrap_or_else(|| &0);. Anda tidak selalu harus menggunakan {} dan menuliskan code yang rumit hanya karena ia adalah sebuah closure. Asalkan Anda menuliskan ||, compiler akan tahu bahwa Anda menggunakan closure.

Method closure yang paling sering digunakan, mungkin adalah .map(). Mari kita lihat lagi. Inilah salah satu cara untuk menggunakannya:

fn main() {
    let num_vec = vec![2, 4, 6];

    let double_vec = num_vec        // ambil num_vec
        .iter()                     // lakukan iterasi pada vec tersebut
        .map(|number| number * 2)   // untuk setiap each item, kalikan dengan 2
        .collect::<Vec<i32>>();     // kemudian, buat sebuah Vec baru dari vec yang sudah di pass melalui chaining method
    println!("{:?}", double_vec);
}

Contoh bagus lainnya adalah dengan .for_each() setelah .enumerate(). Method .enumerate() memberikan iterator dengan nomor index dan item/element. Sebagai contoh: [10, 9, 8] menjadi (0, 10), (1, 9), (2, 8). Type untuk setiap elemenet ini adalah (usize, i32). Sehingga Anda bisa melakukan hal ini:

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()      // lakukan iterasi pada num_vec
        .enumerate() // ambil (index, number)
        .for_each(|(index, number)| println!("Index number {} has number {}", index, number)); // lakukan sesuatu untuk setiap hasil enumerate
}

Hasilnya adalah:

Index number 0 has number 10
Index number 1 has number 9
Index number 2 has number 8

Pada contoh yang ini, kita menggunakan for_each, bukan map. map digunakan untuk melakukan sesuatu ke setiap item dan kemudian lakukan pass pada hasilnya. Sedangkan for_each adalah langsung melakukan sesuatu saat mendapatkan setiap item. Juga, map tidak melakukan apapun kecuali jika Anda menggunakan method seperti collect.

Sebenarnya, ada yang menarik dari iterator. Jika Anda mencoba untuk menggunakan map tanpa method seperti collect, compiler akan memberitahukan Anda bahwa ia tidak melakukan apapun. Ia tidak panic, namun compiler akan sekedar memberitahumu bahwa ia sama sekali tidak melakukan apapun.

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()
        .enumerate()
        .map(|(index, number)| println!("Index number {} has number {}", index, number));

}

Compiler mengatakan:

warning: unused `std::iter::Map` that must be used
 --> src\main.rs:4:5
  |
4 | /     num_vec
5 | |         .iter()
6 | |         .enumerate()
7 | |         .map(|(index, number)| println!("Index number {} has number {}", index, number));
  | |_________________________________________________________________________________________^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

Ini merupakan warning, jadi ia bukanlah error: yang artinya, program tetap berjalan dengan baik meskipun ada teguran. Tapi mengapa num_vec tidak melakukan apapun? Kita bisa melihat typenya untuk mengetahui mengapa hal itu terjadi.

  • let num_vec = vec![10, 9, 8]; Sekarang typenya adalah Vec<i32>.
  • .iter() membuat typenya menjadi Iter<i32>. Sehingga sekarang typenya adalah iterator dengan element bertype i32.
  • .enumerate() membuat typenya menjadi Enumerate<Iter<i32>>. Dan typenya menjadi Enumerate yang berasal dari Iter dengan element i32.
  • .map() menjadikan typenya menjadi Map<Enumerate<Iter<i32>>>. Dan akhirnya menjadi type Map dari Enumerate yang berasal dari Iter dengan elementi32.

Apa yang kita lakukan membuat struktur dari typenya menjadi kompleks dan semakin kompleks. Sehingga Map<Enumerate<Iter<i32>>> ini adalah struktur yang siap digunakan, tetapi hanya jika kita memberi tahu apa yang harus dilakukan. Rust melakukan ini karena ia perlu menjalankan sesuatu dengan cepat. Rust tidak ingin melakukan hal seperti ini:

  • iterasi semua i32 yang berada di dalam Vec
  • kemudian enumerate i32 yang berada dalam iterator
  • kemudiap map semua i32 yang telah di-enumerate

Rust hanya ingin melakukannya dalam 1 langkah, bukan 2, 3 ataupun berkali-kali seperti itu. Sehingga ia hanya membuat strukturnya saja dan menunggu struktur tersebut digunakan. Kemudian jika kita menuliskan .collect::<Vec<i32>>() ia tahu apa yang harus dilakukan, dan mulai mengerjakannya. Inilah apa yang dimaksud dengan iterators are lazy and do nothing unless consumed. Iterator tidak melakukan apapun sampai Anda "mengkonsumsinya" (menggunakannya).

Bahkan Anda bisa membuat sesuatu yang lumayan rumit seperti HashMap menggunakan .collect(), sehingga ia sangatlah powerful. Ini adalah contoh bagaimana menjadikan 2 buah vec menjadi HashMap. Pertama kita buat dua vector, dan kemudian akan kita gunakan .into_iter() pada vector tersebut untuk mendapatkan value dari iterator. Kemudian kita gunakan method .zip(). Method ini mengambil dua iterator dan menyatukannya, seperti zipper. Dan terakhir, kita gunakan .collect() untuk membuat HashMap.

Berikut ini adalah codenya:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // Vec<i32>
    let some_words = vec!["zero", "one", "two", "three", "four", "five"]; // Vec<&str>

    let number_word_hashmap = some_numbers
        .into_iter()                 // sekarang ia adalah iter
        .zip(some_words.into_iter()) // di dalam .zip() kita letakkan iter yang lain. Dan sekarang ia tergabung menjadi satu.
        .collect::<HashMap<_, _>>();

    println!("For key {} we get {}.", 2, number_word_hashmap.get(&2).unwrap());
}

Hasilnya adalah:

For key 2 we get two.

Anda bisa melihat bahwa kita menuliskan <HashMap<_, _>> karena itu adalah informasi yang cukup untuk Rust menentukan apa typenya (yaitu HashMap<i32, &str>). Anda bisa menuliskan .collect::<HashMap<i32, &str>>(); jika Anda menginginkannya, atau Anda dapat menuliskannya seperti ini jika Anda mau:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // Vec<i32>
    let some_words = vec!["zero", "one", "two", "three", "four", "five"]; // Vec<&str>
    let number_word_hashmap: HashMap<_, _> = some_numbers  // Karena kita memberitahukan typenya disini...
        .into_iter()
        .zip(some_words.into_iter())
        .collect(); // maka kita tidak perlu menuliskannya disini
}

Ada method lain seperti .enumerate() yang berguna untuk char, yaitu char_indices(). (Indices artinya "banyak index"). Anda menggunakannya dengan cara yang sama. Anggaplah kita memiliki string besar yang terbuat dari 3 digit angka.

fn main() {
    let numbers_together = "140399923481800622623218009598281";

    for (index, number) in numbers_together.char_indices() {
        match (index % 3, number) {
            (0..=1, number) => print!("{}", number), // print angkanya jika masih ada sisanya
            _ => print!("{}\t", number), // sebaliknya, print angkanya menggunakan tab space
        }
    }
}

Hasilnya adalah 140 399 923 481 800 622 623 218 009 598 281.

|_| in a closure

Terkadang Anda akan menemukan |_| pada sebuah closure. Ini artinya bahwa closure tersebut memerlukan argument/parameter (seperti x), tetapi Anda tidak ingin menggunakannya. Jadinya, |_| berarti "Okay, closure ini mengambil argument, tapi saya tidak memberikannya nama karena saya tidak peduli tentang hal itu".

Ini adalah contoh dimana akan muncul error saat Anda tidak melakukan hal tersebut:

fn main() {
    let my_vec = vec![8, 9, 10];

    println!("{:?}", my_vec.iter().for_each(|| println!("We didn't use the variables at all"))); // ⚠️
}

Rust akan memberikan pesan ini:

error[E0593]: closure is expected to take 1 argument, but it takes 0 arguments
  --> src\main.rs:28:36
   |
28 |     println!("{:?}", my_vec.iter().for_each(|| println!("We didn't use the variables at all")));
   |                                    ^^^^^^^^ -- takes 0 arguments
   |                                    |
   |                                    expected closure that takes 1 argument

Compiler sebenarnya memberikan Anda bantuan/saran berupa pesan seperti berikut:

help: consider changing the closure to take and ignore the expected argument
   |
28 |     println!("{:?}", my_vec.iter().for_each(|_| println!("We didn't use the variables at all")));

Ini merupakan saran yang baik. Jika Anda mengganti || dengan |_| maka programnya akan berjalan.

Helpful methods for closures and iterators

Rust akan menjadi bahasa yang menyenangkan untuk dipelajari setelah Anda merasa nyaman dengan closures. Dengan closure Anda bisa melakukan method berantai (chain method) satu sama lain dan melakukan banyak hal dengan code yang sangat sedikit. Berikut merupakan beberapa closure dan metode yang digunakan dengan closure yang belum kita lihat.

.filter(): Ini memungkinkan Anda menyimpan item dalam iterator yang ingin Anda simpan. Mari kita filter bulan dalam setahun.

fn main() {
    let months = vec!["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

    let filtered_months = months
        .into_iter()                         // membuat sebuah iter
        .filter(|month| month.len() < 5)     // Kita menginginkan nama bulan yang panjangnya kurang dari 5 bytes.
                                             // Kita tahu bahwa setiap huruf itu adalah 1 byte, sehingga kita bisa menggunakan .len()
        .filter(|month| month.contains("u")) // Juga kita hanya mencari nama bulan yang di dalamnya terdapat huruf u
        .collect::<Vec<&str>>();

    println!("{:?}", filtered_months);
}

Hasilnya adalah ["June", "July"].

.filter_map(). Ia disebut filter_map() karena ia melakukan .filter() dan .map(). Closurenya haruslah mengembalikan Option<T>, dan kemudian filter_map() mengambil value untuk setiap Option jika ia adalah Some. Jadi, jika misalnya Anda melakukan .filter_map() pada vec![Some(2), None, Some(3)], kembaliannya adalah [2, 3].

Kita akan membuat contoh dengan struct Company. Setiap company memiliki field name yang bertype String, tapi CEO dari company tersebut mungkin saja baru saja keluar. Sehingga field ceo typenya adalah Option<String>. Kita akan menggunakan .filter_map() pada beberapa company untuk menyimpan nama CEOnya.

struct Company {
    name: String,
    ceo: Option<String>,
}

impl Company {
    fn new(name: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        }; // ceo ditentukan dengan match, dan kemudian kita return Self
        Self {
            name: name.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone() // return clone dari CEO (struct bukanlah Copy)
    }
}

fn main() {
    let company_vec = vec![
        Company::new("Umbrella Corporation", "Unknown"),
        Company::new("Ovintiv", "Doug Suttles"),
        Company::new("The Red-Headed League", ""),
        Company::new("Stark Enterprises", ""),
    ];

    let all_the_ceos = company_vec
        .into_iter()
        .filter_map(|company| company.get_ceo()) // filter_map memerlukan Option<T>
        .collect::<Vec<String>>();

    println!("{:?}", all_the_ceos);
}

Hasilnya adalah ["Unknown", "Doug Suttles"].

Kita tahu bahwa .filter_map() memerlukan Option, bagaimana kalau Result? Tidak apa-apa: ada method bernama .ok() yang akan mengubah Result menjadi Option. Ia dinamakan .ok() karena semua yang ia kirimkan adalah result Ok (informasi Err dihilangkan). Kita ingat bahwa Option adalah Option<T>, sedangkan Result adalah Result<T, E> dengan informasi mengenai Ok dan juga Err. Jadi di saat Anda menggunakan .ok(), semua Err akan menghilang dan menjadi None.

Menggunakan .parse() adalah contoh mudah untuk hal ini, dimana kita mencoba untuk melakukan parse input dari user. .parse() ini akan mengambil &str dan mencoba mengubahnya menjadi f32. Ia akan me-return Result, namun kita menggunakan filter_map(), jadi kita hanya membuang errornya saja. Semua yang Err akan dijadikan None dan selanjutnya akan difilter oleh .filter_map().

fn main() {
    let user_input = vec!["8.9", "Nine point nine five", "8.0", "7.6", "eleventy-twelve"];

    let actual_numbers = user_input
        .into_iter()
        .filter_map(|input| input.parse::<f32>().ok())
        .collect::<Vec<f32>>();

    println!("{:?}", actual_numbers);
}

Hasilnya adalah [8.9, 8.0, 7.6].

Kebalikan dari .ok() adalah .ok_or() dan ok_or_else(). Ini akan mengubah Option menjadi Result. Ia disebut .ok_or() karena Result memberikan Ok or sebuah Err, sehingga perlu bagi Anda untuk memberikan apa value Err-nya nantinya. Ini dikarenakan None pada Option tidak memiliki informasi apapun. Juga, Anda bisa melihat bahwa bagian else pada nama method yang lainnya berarti bahwa ia memiliki closure.

Kita bisa mengambil Option dari struct Company dan mengubahnya menjadi Result dengan cara ini. Untuk error handling jangka panjang, ada baiknya apabila Anda membuat type error Anda sendiri. Namun untuk sekarang ini, kita cukup memberikannya pesan error, sehingga ia menjadi Result<String, &str>.

// Semua yang ditulis sebelum main() masih sama seperti program sebelumnya
struct Company {
    name: String,
    ceo: Option<String>,
}

impl Company {
    fn new(name: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        };
        Self {
            name: name.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let company_vec = vec![
        Company::new("Umbrella Corporation", "Unknown"),
        Company::new("Ovintiv", "Doug Suttles"),
        Company::new("The Red-Headed League", ""),
        Company::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![]; // Anggap saja kita perlu mengumpulkan hasil errornya juga

    company_vec
        .iter()
        .for_each(|company| results_vec.push(company.get_ceo().ok_or("No CEO found")));

    for item in results_vec {
        println!("{:?}", item);
    }
}

Baris yang ini mengalami banyak perubahan:


#![allow(unused)]
fn main() {
// 🚧
.for_each(|company| results_vec.push(company.get_ceo().ok_or("No CEO found")));
}

Ini artinya: "untuk setiap company, gunakan get_ceo(). Jika Anda mendapatkannya, maka pass valuenya ke dalam Ok. dan jika Anda tidak mendapatkannya, pass "No CEO found" ke dalam Err. Kemudian push Result tersebut ke dalam vec."

Sehingga, saat kita mencetak results_vec, kita mendapatkan ini:

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found")
Err("No CEO found")

Sehingga kita sekarang memiliki 4 entry. Sekarang, mari kita coba .ok_or_else() agar kita bisa menggunakan closure dan mendapatkan pesan error yang lebih baik. Disini kita bisa menggunkan format! untuk membuat sebuah String, dan menaruh nama company di dalamnya. Kemudian kita return String-nya.

// Semua yang ditulis sebelum main() masih sama seperti program sebelumnya
struct Company {
    name: String,
    ceo: Option<String>,
}

impl Company {
    fn new(name: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            name => Some(name.to_string()),
        };
        Self {
            name: name.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let company_vec = vec![
        Company::new("Umbrella Corporation", "Unknown"),
        Company::new("Ovintiv", "Doug Suttles"),
        Company::new("The Red-Headed League", ""),
        Company::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![];

    company_vec.iter().for_each(|company| {
        results_vec.push(company.get_ceo().ok_or_else(|| {
            let err_message = format!("No CEO found for {}", company.name);
            err_message
        }))
    });

    for item in results_vec {
        println!("{:?}", item);
    }
}

Hasil program tersebut adalah:

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found for The Red-Headed League")
Err("No CEO found for Stark Enterprises")

.and_then() adalah method yang sangat membantu yang mana ia akan mengambil Option, kemudian membuat Anda bisa melakukan sesuatu terhadap valuenya dan pass valuenya. Jadi, inputnya adalah Option, dan outputnya pula adalah Option. Ini sama seprti "unwrap, kemudian lakukan sesuatu, kemudian wrap lagi" dengan cara yang lebih aman.

Contoh mudahnya adalah sebuah angka yang kita dapatkan dari sebuah vec menggunakan .get(), karena kembaliannya adalah Option. Sekarang kita bisa melakukan pass valuenya ke and_then(), dan melakukan perhitungan matematis padanya jika ia adalah Some. Jika ia adalah None, maka None yang akan di-pass.

fn main() {
    let new_vec = vec![8, 9, 0]; // vec yang berisi angka-angka

    let number_to_add = 5;       // gunakan ini untuk melakukan operasi matematis
    let mut empty_vec = vec![];  // resultnya akan dimasukkan ke sini


    for index in 0..5 {
        empty_vec.push(
            new_vec
               .get(index)
                .and_then(|number| Some(number + 1))
                .and_then(|number| Some(number + number_to_add))
        );
    }
    println!("{:?}", empty_vec);
}

Hasil cetaknya adalah [Some(14), Some(15), Some(6), None, None]. Anda bisa melihat bahwa None tidak difilter (dibuang keluar), ia ikut di-pass ke dalam vector.

.and() semacam bool pada Option. Anda bisa mencocokkan banyak Option ke Option yang lainnya, dan jika mereka semua adalah Some maka ia akan mengembalikan Some yang terakhir. Dan jika salah satunya adalah None, maka ia akan mengembalikan None.

Pertama-tama, ini adalah contoh bool untuk membantu Anda mendapatkan gambarannya. Anda bisa melihat bahwa jika Anda menggunakan && (and), meskipun hanya ada satu false maka hasilnya akan false.

fn main() {
    let one = true;
    let two = false;
    let three = true;
    let four = true;

    println!("{}", one && three); // prints true
    println!("{}", one && two && three && four); // prints false
}

Hal ini juga berlaku pada .and(). Bayangkan kita melakukan 5 operasi .and() dan menaruh hasilnya ke dalam Vec<Option<&str>>.

fn main() {
    let first_try = vec![Some("success!"), None, Some("success!"), Some("success!"), None];
    let second_try = vec![None, Some("success!"), Some("success!"), Some("success!"), Some("success!")];
    let third_try = vec![Some("success!"), Some("success!"), Some("success!"), Some("success!"), None];

    for i in 0..first_try.len() {
        println!("{:?}", first_try[i].and(second_try[i]).and(third_try[i]));
    }
}

Program di atas akan menunjukkan index mana saja yang selalu mendapatkan Some dari 3 vec tersebut. Hasilnya adalah:

None
None
Some("success!")
Some("success!")
None

Pada index ke-0 hasilnya adalah None karena ada None pada index ke-0 di vec second_try. Index ke-1 adalah None karena ada None pada vec first_try. Selanjutnya, hasilnya adalah Some("success!") karena tidak ada None pada vec first_try, second try, ataupun third_try.

.any() dan .all() sangat mudah digunakan dalam iterator. Ia mengembalikan bool tergantung dari inputan Anda. Pada contoh ini kita membuat vec yang sangat besar (sekitar 20,000 item/element) dengan menggunakan karakter dari 'a' sampai '働'. Kemudian kita buat sebuah function untuk memeriksa apakah ada karakter di dalamnya.

selanjutnya kita buat sebuah vec yang lebih kecil and dan memeriksa apakah semuanya adalah alphabet (dengan menggunakan method .is_alphabetic()). Kemudian kita tanyakan apakah semua karakter kurang dari karakter Korea '행'.

Perhatikan juga bahwa Anda menggunakan reference, karena .iter() memberikan reference dan Anda menggunakan & untuk dibandingkan dengan & lainnya.

fn in_char_vec(char_vec: &Vec<char>, check: char) {
    println!("Is {} inside? {}", check, char_vec.iter().any(|&char| char == check)); // reference destructure 
}

fn main() {
    let char_vec = ('a'..'働').collect::<Vec<char>>();
    in_char_vec(&char_vec, 'i');
    in_char_vec(&char_vec, '뷁');
    in_char_vec(&char_vec, '鑿');

    let smaller_vec = ('A'..'z').collect::<Vec<char>>();
    println!("All alphabetic? {}", smaller_vec.iter().all(|&x| x.is_alphabetic())); // reference destructure 
    println!("All less than the character 행? {}", smaller_vec.iter().all(|&x| x < '행')); // reference destructure 
}

Hasilnya adalah:

Is i inside? true
Is 뷁 inside? false
Is 鑿 inside? false
All alphabetic? false
All less than the character 행? true

Ah ya, .any() hanya memeriksa sampai menemukan satu item yang cocok, lalu berhenti. Ia tidak akan memeriksa semuanya jika sudah menemukan kecocokan. Jika Anda menggunakan .any() pada Vec, mungkin adalah ide yang bagus untuk push item yang mungkin cocok ke bagian depan. Atau Anda bisa menggunakan .rev() setelah .iter() untuk me-reverse (membalik) iteratornya. Contohnya seperti ini:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);
}

Vec di atas memilik 1000 buah 6 yang kemudian diisi dengan sebuah 5. Anggap saja kita ingin menggunakan .any() untuk melihat apakah ia berisi 5. Pertama, kita pastikan dulu bahwa .rev() berjalan dengan benar. Diingat lagi, sebuah Iterator selalu memiliki .next() yang memungkinkan Anda memeriksa apa yang dilakukannya setiap saat.

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut iterator = big_vec.iter().rev();
    println!("{:?}", iterator.next());
    println!("{:?}", iterator.next());
}

Hasilnya adalah:

Some(5)
Some(6)

Yup, program kita berjalan dengan benar. Ada satu Some(5) dan 1000 Some(6) setelahnya. Jadinya kita menulisnya seperti ini:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    println!("{:?}", big_vec.iter().rev().any(|&number| number == 5));
}

Dan karena ia menggunakan .rev(), ia hanya memanggil .next() sekali saja dan berhenti. Jika kita tidak menggunakan .rev() maka ia akan memanggil .next() 1001 kali sebelum programnya berhenti. Codenya seperti ini:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut counter = 0; // Mulai menghitung
    let mut big_iter = big_vec.into_iter(); // ubah big_vec menjadi Iterator

    loop {
        counter +=1;
        if big_iter.next() == Some(5) { // tetap memanggil .next() sampai kita mendapatkan Some(5)
            break;
        }
    }
    println!("Final counter is: {}", counter);
}

Hasilnya adalah Final counter is: 1001, sehingga kita tahu bahwa ia akan memanggil .next() 1001 kali sebelum ia menemukan 5.

.find() memberitahu kita jika iterator memiliki sesuatu, dan .position() memberi tahu kita dimana lokasinya. .find() berbeda dari .any() karena ia mengembalikan Option dengan value di dalamnya (atau None). Sedangkan .position() juga adalah Option beserta angka yang merepresentasikan posisinya, atau None. Dengan kata lain:

  • .find(): "Saya akan mencoba mencarikannya untuk Anda"
  • .position(): "Saya akan mencoba menemukan dimana tempatnya untuk Anda"

Berikut adalah contohnya:

fn main() {
    let num_vec = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100];

    println!("{:?}", num_vec.iter().find(|&number| number % 3 == 0)); // method find mengambil reference, sehingga kita berikan ia &number
    println!("{:?}", num_vec.iter().find(|&number| number * 2 == 30));

    println!("{:?}", num_vec.iter().position(|&number| number % 3 == 0));
    println!("{:?}", num_vec.iter().position(|&number| number * 2 == 30));

}

Ia akan mencetak:

Some(30) // This is the number itself
None // No number inside times 2 == 30
Some(2) // This is the position
None

Dengan menggunakan .cycle() Anda bisa membuat iterator yang terus menerus melakukan loop. Iterator seperti ini bekerja dengan baik dengan method .zip() untuk membuat sesuatu yang baru, seperti pada contoh ini yang mana akan membuat Vec<(i32, &str)>:

fn main() {
    let even_odd = vec!["even", "odd"];

    let even_odd_vec = (0..6)
        .zip(even_odd.into_iter().cycle())
        .collect::<Vec<(i32, &str)>>();
    println!("{:?}", even_odd_vec);
}

Jadi meskipun method .cycle() semestinya tidak berhenti, iterator yang lain hanya menjalankannya 6 kali di saat melakukan zip antara vec even_odd dan iterator 0..6. Itu berarti bahwa iterator yang dibuat oleh .cycle() tidak lagi memanggil .next(), sehingga ia selesai setelah dipanggil sebanyak 6 kali. Outputnya adalah seperti berikut:

[(0, "even"), (1, "odd"), (2, "even"), (3, "odd"), (4, "even"), (5, "odd")]

Hal serupa dapat dilakukan dengan pada range yang tidak memiliki akhir. Jika Anda menuliskan 0.. maka Anda membuat sebuah range yang tidak berhenti. Anda bisa menggunakannya dengan sangat mudah, seperti ini:

fn main() {
    let ten_chars = ('a'..).take(10).collect::<Vec<char>>();
    let skip_then_ten_chars = ('a'..).skip(1300).take(10).collect::<Vec<char>>();

    println!("{:?}", ten_chars);
    println!("{:?}", skip_then_ten_chars);
}

Keduanya akan mencetak 10 characters, namun pada cetakan kedua ia melakukan skip sebanyak 1300, dan mencetak 10 huruf dalam aksara Armenia.

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
['յ', 'ն', 'շ', 'ո', 'չ', 'պ', 'ջ', 'ռ', 'ս', 'վ']

Method terkenal lainnya adalah .fold(). Method ini sering digunakan untuk menjumlahkan elemen-elemen yang berada di dalam iterator, tapi tidak hanya itu saja, Anda juga bisa melakukan hal lain. Method ini mirip dengan .for_each(). Pada .fold(), Pertama-tama Anda menambahkan value awalan (jika Anda ingin menjumlahkan setiap element, maka valuenya 0), kemudian tuliskan koma, dan selanjutnya tuliskan closure. Closure memberikan Anda 2 hal, yaitu total (yang dihitung sejauh iterator dijalankan), dan element selanjutnya. Dibawah ini adalah contoh menggunakan .fold() untuk menjumlahkan setiap element di dalam vec.

fn main() {
    let some_numbers = vec![9, 6, 9, 10, 11];

    println!("{}", some_numbers
        .iter()
        .fold(0, |total_so_far, next_number| total_so_far + next_number)
    );
}

Jadi, berikut penjelasannya:

  • Pada langkah 1, ia dimulai dengan 0 dan menambahkannya dengan angka selanjutnya, yaitu 9.
  • Kemudian diambillah 9 dan ditambahkan dengan element selanjutnya, yaitu 6: totalnya 15.
  • Kemudian diambillah 15, dan ditambahkan dengan element selanjutnya, yaitu 9: totalnya 24.
  • Kemudian diambillah 24, dan ditambahkan dengan element selanjutnya, yaitu 10: totalnya 34.
  • Dan terakhir, diambillah 34, dan ditambahkan dengan 11: sehingga hasil akhirnya adalah 45. Jadinya ia akan mencetak 45.

Tidak hanya untuk hal seperti itu, ini adalah contoh dimana kita bisa menambahakan '-' ke setiap karakter untuk membuat String.

fn main() {
    let a_string = "I don't have any dashes in me.";

    println!(
        "{}",
        a_string
            .chars() // sekarang ia sudah menjadi iterator
            .fold("-".to_string(), |mut string_so_far, next_char| { // mulai dengan String "-". Jadiak ia sebagai mutable setiap saat bersama dengan karakter berikutnya
                string_so_far.push(next_char); // Push terlebih dahulu charnya, kemudian '-'
                string_so_far.push('-');
                string_so_far} // Jangan lupa untuk pass hasilnya ke loop selanjutnya
            ));
}

Hasilnya adalah:

-I- -d-o-n-'-t- -h-a-v-e- -a-n-y- -d-a-s-h-e-s- -i-n- -m-e-.-

Dan masih banyak method yang mudah untuk digunakan seperti:

  • .take_while() yang mana akan mengambil value dari iterator selama kondisinya selalu true (contohnya take while x > 5)
  • .cloned() yang mana akan membuat clone didalam iterator.Ini akan mengubah reference menjadi value.
  • .by_ref() yang mana membuat sebuah iterator mengambil reference. Ini bagus untuk memastikan bahwa Anda bisa menggunakan sebuah Vec (atau sesuatu yang serupa) setelah Anda menggunakannya untuk membuat iterator.
  • Method _while lainnya: .skip_while(), .map_while(), dan seterusnya
  • .sum(): menjumlahkan apapun yang ada di dalamnya.

.chunks() dan .windows() adalah dua cara untuk memotong vector menjadi ukuran sesuai yang Anda inginkan. Anda tuliskan ukuran yang Anda inginkan di dalam bracket. Katakanlah, Anda memiliki vector yang berisi 10 item, dan Anda ingin ukurannya adalah 3. Ia akan bekerja seperti ini:

  • .chunks() akan memberikanmu 4 potong: [0, 1, 2], kemudian [3, 4, 5], kemudian [6, 7, 8], dan terakhir [9]. Jadinya, ia akan mencoba untuk membuat sebuah potongan dari 3 item, tapi jika ia tidak mencapai 3 buah, ia tidak panic. Ia hanya akan memberi apa yang tersisa.

  • .windows() pertama-tama akan memberimu potongan [0, 1, 2]. Kemudian ia akan berpindah maju selangkah dan memberi Anda potongan [1, 2, 3]. Ia akan melakukan itu sampai akhirnya ia mencapai potongan terakhir yang anggotanya 3 item, dan kemudian berhenti.

So let's use them on a simple vector of numbers. It looks like this:

fn main() {
    let num_vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

    for chunk in num_vec.chunks(3) {
        println!("{:?}", chunk);
    }
    println!();
    for window in num_vec.windows(3) {
        println!("{:?}", window);
    }
}

Hasilnya adalah:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[0]

[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 0]

Perlu diketahui, .chunks() akan panic jika Anda tidak memberikannya apapun. Anda bisa menulis .chunks(1000) untuk sebuah vector yang hanya memiliki satu item, tapi Anda tidak bisa menulis .chunks() dengan sesuatu yang panjangnya 0.

.match_indices() memungkinkan Anda menarik semua yang ada di dalam sebuah String atau &str yang cocok dengan input yang Anda berikan, dan ia akan memberikan Anda indexnya juga. Ini mirip seperti .enumerate() karena ia memberikan return berupa tuple yang berisi dua item.

fn main() {
    let rules = "Rule number 1: No fighting. Rule number 2: Go to bed at 8 pm. Rule number 3: Wake up at 6 am.";
    let rule_locations = rules.match_indices("Rule").collect::<Vec<(_, _)>>(); // Ini adalah Vec<usize, &str>, tapi kita biarkan Rust yang akan menentukannya
    println!("{:?}", rule_locations);
}

Hasilnya adalah:

[(0, "Rule"), (28, "Rule"), (62, "Rule")]

.peekable() memungkinkan Anda membuat iterator di mana Anda dapat melihat (mengintip) item berikutnya. Ini seperti memanggil .next() (yang mana ia akan memberikan Option), hanya saja iteratornya tidak bergerak, sehingga Anda dapat menggunakannya sebanyak yang Anda inginkan. Anda sebenarnya dapat menganggap peekable ini sebagai "stoppable", karena Anda dapat berhenti selama yang Anda inginkan. Berikut adalah contoh dimana kita menggunakan .peek() tiga kali pada setiap item. Kita bisa menggunakan .peek() selamanya sampai kita menggunakan .next() untuk pindah ke item berikutnya.

fn main() {
    let just_numbers = vec![1, 5, 100];
    let mut number_iter = just_numbers.iter().peekable(); // Ini sebenarnya membuat type iterator yang biasa disebut sebagai Peekable

    for _ in 0..3 {
        println!("I love the number {}", number_iter.peek().unwrap());
        println!("I really love the number {}", number_iter.peek().unwrap());
        println!("{} is such a nice number", number_iter.peek().unwrap());
        number_iter.next();
    }
}

Hasilnya adalah:

I love the number 1
I really love the number 1
1 is such a nice number
I love the number 5
I really love the number 5
5 is such a nice number
I love the number 100
I really love the number 100
100 is such a nice number

Ini adalah contoh lain dimana kita menggunakan .peek() untuk mencocokka sebuah item. Setelah kita selesai menggunakannya, kita panggil .next().

fn main() {
    let locations = vec![
        ("Nevis", 25),
        ("Taber", 8428),
        ("Markerville", 45),
        ("Cardston", 3585),
    ];
    let mut location_iter = locations.iter().peekable();
    while location_iter.peek().is_some() {
        match location_iter.peek() {
            Some((name, number)) if *number < 100 => { // .peek() memberikan kita reference, sehingga kita memerlukan *
                println!("Found a hamlet: {} with {} people", name, number)
            }
            Some((name, number)) => println!("Found a town: {} with {} people", name, number),
            None => break,
        }
        location_iter.next();
    }
}

Outputnya:

Found a hamlet: Nevis with 25 people
Found a town: Taber with 8428 people
Found a hamlet: Markerville with 45 people
Found a town: Cardston with 3585 people

Dan terakhir, ini adalah contoh dimana kita juga bisa menggunakan .match_indices(). Pada contoh ini, kita meletakkan nama ke dalam struct tergantung pada jumlah spasi di &str.

#[derive(Debug)]
struct Names {
    one_word: Vec<String>,
    two_words: Vec<String>,
    three_words: Vec<String>,
}

fn main() {
    let vec_of_names = vec![
        "Caesar",
        "Frodo Baggins",
        "Bilbo Baggins",
        "Jean-Luc Picard",
        "Data",
        "Rand Al'Thor",
        "Paul Atreides",
        "Barack Hussein Obama",
        "Bill Jefferson Clinton",
    ];

    let mut iter_of_names = vec_of_names.iter().peekable();

    let mut all_names = Names { // buat sebuah struct Names yang kosong
        one_word: vec![],
        two_words: vec![],
        three_words: vec![],
    };

    while iter_of_names.peek().is_some() {
        let next_item = iter_of_names.next().unwrap(); // kita bisa menggunakan .unwrap() karena kita tahu bahwa ia adalah Some
        match next_item.match_indices(' ').collect::<Vec<_>>().len() { // Buat sebuah vec menggunakan .match_indices dan periksa panjangnya
            0 => all_names.one_word.push(next_item.to_string()),
            1 => all_names.two_words.push(next_item.to_string()),
            _ => all_names.three_words.push(next_item.to_string()),
        }
    }

    println!("{:?}", all_names);
}

Ia akan mencetak:

Names { one_word: ["Caesar", "Data"], two_words: ["Frodo Baggins", "Bilbo Baggins", "Jean-Luc Picard", "Rand Al\'Thor", "Paul Atreides"], three_words:
["Barack Hussein Obama", "Bill Jefferson Clinton"] }