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 adalahVec<i32>
..iter()
membuat typenya menjadiIter<i32>
. Sehingga sekarang typenya adalah iterator dengan element bertypei32
..enumerate()
membuat typenya menjadiEnumerate<Iter<i32>>
. Dan typenya menjadiEnumerate
yang berasal dariIter
dengan elementi32
..map()
menjadikan typenya menjadiMap<Enumerate<Iter<i32>>>
. Dan akhirnya menjadi typeMap
dariEnumerate
yang berasal dariIter
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 selalutrue
(contohnyatake 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 sebuahVec
(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"] }