A.50. Trait ➜ Iterator
Iterator adalah salah satu trait dan tipe data custom penting pada Rust programming, gunanya untuk iterasi data. Di chapter ini kita akan mempelajarinya beserta beberapa module item lainnya yang masih relevan dengan topik Iterator.
A.50.1. Iterator
& IntoIterator
Iterator
adalah sebuah nama yang dipakai sebagai nama module item dalam Rust Standard Library maupun Rust Core Library, digunakan untuk iterasi data dan operasi lain yang berhubungan dengannya.
Iterator sendiri merupakan istilah untuk object atau trait yang bisa diiterasi, baik menggunakan keyword for in
ataupun menggunakan method iterator seperti for_each
(yang juga akan kita bahas di sini).
◉ Trait Iterator
& IntoIterator
Dalam perulangan menggunakan keyword for in
, tipe data variabel yang digunakan harus memiliki trait Iterator
. Jika tidak, pasti error muncul.
Tipe data slice di Rust by default mengadopsi trait bernama IntoIterator
, yang trait ini digunakan untuk konversi data bertipe slice ke bentuk iterator.
Dalam praktiknya, tidak perlu mengakses method tertentu untuk mendapatkan object iterator suatu data. Cukup gunakan variabel tipe data slice pada keyword for in
, maka proses konversi ke bentuk iterator akan dilakukan oleh Rust secara otomatis dibalik layar.
Contoh:
// array
let data_arr = ["a", "b", "c", "d"];
for e in &data_arr {
print!("{e:?} ")
}
println!();
// vector
let data_vec = vec!["a", "b", "c", "d"];
for e in &data_vec {
print!("{e:?} ")
}
println!();
// slice
let data_vec = vec!["a", "b", "c", "d"];
let data_slice_vec = &data_vec[..];
for e in data_slice_vec {
print!("{e:?} ")
}
Dianjurkan untuk selalu menggunakan teknik Borrowing dalam penggunaan keyword for in
pada tipe data yang karakteristiknya adalah Move Semantics, hal ini karena dalam penerapan keyword tersebut, terjadi proses konversi tipe data dengan trait IntoIterator
ke bentuk Iterator
.
Jika data yang digunakan bukan data borrow, maka pasti owner berpindah.
◉ Struct Iterator
Selain trait Iterator
, ada juga tipe data struct bernama sama, yaitu Iterator
. Perbedaan antara trait vs struct Iterator
adalah tipe struct Iterator
memiliki beberapa method untuk keperluan iterasi object. Jadi dengan memanfaatkan method-method tersebut, kita bisa melakukan iterasi data dan operasi sejenisnya tanpa menggunakan for in
.
Semua tipe data slice bisa dikonversi ke tipe data Iterator
, caranya dengan mengakses method iter
(atau method chars
khusus untuk tipe data String
).
Dari object iterator tersebut, perulangan bisa dilakukan via keyword for in
, atau dengan memanfaatkan method bernama for_each
. Perbedaannya pada iterasi menggunakan for_each
, block perulangan dituliskan dalam bentuk closure.
// array
let data_arr = ["a", "b", "c", "d"];
let iterator_arr = data_arr.iter();
iterator_arr.for_each(|e| {
print!("{e:?} ")
});
println!();
// vector
let data_vec = vec!["a", "b", "c", "d"];
data_vec.iter().for_each(|e| {
print!("{e:?} ")
});
println!();
// slice from vector
let data_vec = vec!["a", "b", "c", "d"];
let data_slice_vec = &data_vec[..];
data_slice_vec.iter().for_each(|e| {
print!("{e:?} ")
});
println!();
// slice from String
let data_str = "abcd".to_string();
let data_borrow_str = &data_str;
data_borrow_str.chars().for_each(|e| {
print!("{e:?} ")
});
println!();
Bisa dilihat pada contoh di atas, data slice diambil objek Iterator-nya menggunakan method iter
(atau chars
khusus untuk tipe data String
), kemudian di-iterasi menggunakan method for_each
.
A.50.2. Pemanfaatan tipe data Iterator
Tipe data Iterator
memiliki cukup banyak method untuk keperluan operasi data iterator, contohnya seperti for_each
, map
, filter
, dan lainnya.
◉ Praktik ke-1
Sebagai contoh, kita akan buat sebuah program sederhana yang melakukan operasi kuadrat pada elemen tiap-tiap array.
let data_arr = [1, 3, 5];
let doubles: Vec<i32> = data_arr
.iter()
.map(|e| e * e)
.collect();
println!("{doubles:?}");
Penjelasan:
- Variabel
data_arr
yang bertipe data[i32; 3]
dikonversi ke tipe dataIterator
menggunakan methoditer
. - Object
Iterator
kemudian di-iterasi menggunakan methodmap
dan nilai baliknya dijadikan replacement data elemen tersebut. - Object
Iterator
kemudian di-collect data-nya ke bentukVec<i32>
menggunakan methodcollect
.
◉ Praktik ke-2
Contoh lain penerapan method iterator bisa dilihat pada kode berikut:
let data_vec = vec!["1", "2", "3", "4", "a"];
let numbers: Vec<i32> = data_vec
.iter()
.map(|e| -> i32 {
match e.parse::<i32>() {
Ok(n) => n,
Err(_) => 0,
}
})
.filter(|e| *e > 0 && *e % 2 == 0)
.rev()
.collect::<Vec<i32>>();
println!("{numbers:?}");
Program di atas melakukan beberapa hal:
Data
data_vec
yang merupakan koleksi string di-konversi menjadi objectIterator
menggunakan methoditer
.Kemudian method
map
diakses. Setiap elemendata_vec
di-iterasi, kemudian dikonversi dariString
kei32
, lalu dijadikan replacement data elemen tersebut. Jika proses konversi gagal, maka angka0
digunakan sebagai data element tersebut.- Sampai sini, data yang sebelumnya
["1", "2", "3", "4", "a"]
sekarang menjadi[1, 2, 3, 4, 0]
.
- Sampai sini, data yang sebelumnya
Selanjutnya, dilakukan proses filtering menggunakan method
filter
dengan kondisi*e > 0 && *e % 2 == 0
yang kurang lebih artinya, jika nilai dereferencee
lebih besar dari0
dan nilai tersebut adalah genap, maka filter bernilaitrue
.- Sampai sini, data yang sebelumnya
[1, 2, 3, 4, 0]
sekarang menjadi[2, 4]
.
- Sampai sini, data yang sebelumnya
Kemudian, data kolektif tersebut dibalik urutan elemennya menggunakan method
rev
.- Sampai sini, data yang sebelumnya
[2, 4]
sekarang menjadi[4, 2]
.
- Sampai sini, data yang sebelumnya
Terakhir data di-collect menggunakan method
collect
dengan ditentukan tipe data masing-masing elemen adalahVec<i32>
.
Hasilnya ketika di run:
A.50.3. Method tipe data Iterator
◉ Method iter
, iter_mut
, into_iter
Ketiga method ini berguna untuk konversi data slice ke bentuk Iterator
, perbedannya:
Method
iter
mengembalikan dataIterator
yang isinya adalah reference (&T
) setiap element. Contoh penerapan:let data_vec = vec![1, 2, 3, 4];
data_vec.iter().for_each(|d| {
println!("{}", *d * 2)
});
for d in &data_vec {
println!("{}", *d * 2)
}Method
iter_mut
mengembalikan dataIterator
yang isinya adalah mutable reference (&mut T
) setiap element. Contoh penerapan:let mut data_vec = vec![1, 2, 3, 4];
data_vec.iter_mut().for_each(|d| {
*d = *d * 2;
});
println!("{:?}", data_vec);
for d in &mut data_vec {
*d = *d * 2;
}
println!("{:?}", data_vec);Method
into_iter
mengkonversi data slice ke bentukIterator
(move semantics).let data_vec = vec![1, 2, 3, 4];
data_vec.into_iter().for_each(|d| {
println!("{}", d * 2)
});
// statement di bawah ini menghasilkan error,
// karena ownership `data_vec` telah berpindah setelah method `into_iter` dipanggil
// println!("{}", data_vec)
◉ Method collect
Method ini digunakan untuk collecting data Iterator ke bentuk tertentu. Contoh penerapannya bisa dilihat pada kode berikut ini.
Data data_vec
yang merupakan vektor bertipe Vec<i32>
dikonversi ke bentuk Iterator
kemudian di-collect datanya.
let data_vec: Vec<i32> = vec![1, 2, 3, 4];
let result: Vec<&i32> = data_vec.iter().collect();
println!("{:?}", result);
// [1, 2, 3, 4]
Method iter
menghasilkan object Iterator
yang menampung reference &T
setiap element slice. Hal ini membuat penerapan method collect
menghasilkan data bertipe Vec<&i32>
(bukan Vec<i32>
).
◉ Method map
Method map
digunakan untuk mapping element setiap data iterator ke nilai baru.
Sebelumnya kita telah mempelajari method iter
dan collect
yang keduanya jika dikombinasikan dan digunakan pada tipe data Vec<i32>
hasilnya adalah data bertipe Vec<&i32>
. Dengan memanfaatkan method map
, tipe data setiap element bisa di-mapping ke nilai baru dengan tipe data berbeda, misalnya menjadi Vec<i32>
.
let data_vec: Vec<i32> = vec![1, 2, 3, 4];
let result1: Vec<&i32> = data_vec.iter().collect();
println!("{:?}", result1);
// [1, 2, 3, 4]
let result2: Vec<i32> = data_vec
.iter()
.map(|d: &i32| -> i32 { *d })
.collect();
println!("{:?}", result2);
// [1, 2, 3, 4]
Contoh lainnya bisa dilihat pada section A.50.2. Pemanfaatan tipe data Iterator
, di situ terdapat operasi mapping data slice numerik ke bentuk yang sama tapi nilai setiap element adalah kuadrat, dan ke bentuk lain dengan tipe data berbeda.
◉ Method rev
rev
kependekan dari reverse, digunakan untuk membalikan urutan data slice.
let data_vec = vec![1, 2, 3, 4];
println!("{:?}", data_vec);
// [1, 2, 3, 4]
let result: Vec<&i32> = data_vec.iter().rev().collect();
println!("{:?}", result);
// [4, 3, 2, 1]
◉ Method filter
Method filter
digunakan untuk memfilter element data slice. Data kolektif diiterasi kemudian dicek menggunakan closure, jika nilai balik bertipe true
maka elemen tersebut masuk dalam dalam hasil filter, selebihnya maka dianggap tidak memenuhi kondisi filter dan elemen di-exclude.
let data_vec = vec![1, 2, 3, 4];
let odd: Vec<&i32> = data_vec.iter().filter(|d| *d % 2 != 0).collect();
println!("odd numbers: {:?}", odd);
// odd numbers: [1, 3]
let even: Vec<&i32> = data_vec.iter().filter(|d| *d % 2 == 0).collect();
println!("even numbers: {:?}", even);
// odd numbers: [2, 4]
◉ Method cloned
Digunakan untuk cloning data slice secara keseluruhan tanpa mengubah tipe data. Hasilnya adalah data dengan skema sama persis (dalam bentuk Iterator
) tapi berbeda owner.
let data_vec = vec![1, 2, 3, 4];
let result: Vec<i32> = data_vec.iter().cloned().collect();
println!("{:?}", result);
// [1, 2, 3, 4]
◉ Method copied
Secara high-level copied
menghasilkan output yang sama dengan cloned
. Namun jika dibahas dari sisi management memory-nya, ada perbedaan yang cukup besar.
let data_vec = vec![1, 2, 3, 4];
let result: Vec<i32> = data_vec.iter().copied().collect();
println!("{:?}", result);
// [1, 2, 3, 4]
Lebih detailnya mengenai clone vs copy akan dibahas pada chapter Copy, Clone, Move, Drop
◉ Method cmp
cmp
kependekan dari compare, digunakan untuk membandingkan 2 buah data iterasi dengan nilai balik bertipe enum Ordering
. Dari tipe data enum tersebut nantinya bisa dicek apakah 2 buah data slice tersebut sama (secara Lexicographical).
let data_vec1 = vec![1, 2, 3, 4];
let data_vec2 = vec![1, 2, 3, 4];
let result = data_vec1.iter().cmp(data_vec2.iter());
println!("{:?}", result.is_eq());
// true
Enum Ordering
memiliki beberapa method, salah satunya adalah is_eq
yang mengembalikan nilai true
jika dua buah data slice tersebut adalah sama.
Lebih detailnya mengenai
Ordering
akan dibahas pada chapter Enum Ordering
◉ Method count
Digunakan untuk melihat size dari elemen Iterator.
let data_vec = vec![1, 2, 3, 4];
let length = data_vec.iter().count();
println!("{:?}", length);
// 4
◉ Method eq
, ne
, gt
, ge
, lt
, le
6 Method ini digunakan untuk komparasi dua buah slice. Penggunakan 6 method ini merupakan alternatif selain menggunakan method cmp
.
Sebagai contoh, penerapan method eq
berikut untuk untuk mengecek apakah 2 buah data slice adalah sama (secara Lexicographical).
let data_vec1 = vec![1, 2, 3, 4];
let data_vec2 = vec![1, 2, 3, 4];
let result = data_vec1.iter().eq(data_vec2.iter());
println!("{:?}", result);
// true
Kode di atas adalah ekuivalen dengan kode berikut:
let data_vec1 = vec![1, 2, 3, 4];
let data_vec2 = vec![1, 2, 3, 4];
let result = data_vec1.iter().cmp(data_vec2.iter());
println!("{:?}", result.is_eq());
// true
Berikut merupakan kegunaan tiap-tiap method di atas:
- Method
eq
digunakan untuk equal lexicographical comparison - Method
ne
digunakan untuk not equal lexicographical comparison - Method
gt
digunakan untuk greater than lexicographical comparison - Method
ge
digunakan untuk greater than or equal lexicographical comparison - Method
lt
digunakan untuk lower than lexicographical comparison - Method
le
digunakan untuk lower than or equal lexicographical comparison
◉ Method find
Digunakan untuk mencari apakah suatu elemen ada atau tidak dengan kondisi pencarian dituliskan dalam bentuk closure. Nilai balik method ini bertipe Option
.
Sebagai contoh, perhatikan kode berikut. Method find
digunakan untuk mencari elemen yang nilainya adalah 4
.
let data_vec = vec![1, 2, 3, 4];
let result = match data_vec.iter().find(|d: &&i32| **d == 4) {
Some(d) => *d,
None => 0
};
println!("{:?}", result);
// 4
◉ Method last
Digunakan untuk mengambil elemen terakhir data Iterator. Method last
ini mengembalikan nilai balik bertipe Option
.
let data_vec = vec![1, 2, 3, 4];
let result: &i32 = data_vec.iter().last().unwrap();
println!("{:?}", result);
// 4
Satu hal yang unik perihal notasi closure method find
, parameter closure adalah bertipe &&T
. Dari tipe tersebut, untuk mengambil underlying value gunakan operator dereference dua kali. Contohnya pada kode di atas, d
bertipe data &&i32
, untuk mengambil nilai sebenarnya digunakan **d
.
◉ Method fold
Method ini digunakan untuk mengkonversi setiap element Iterator
menjadi sebuah akumulator yang direpresentasikan oleh 1 buah variabel.
konsep
fold
milik Rust mirip sepertireduce
pada bahasa pemrograman lain
Pada contoh berikut, kita coba terapkan method fold
untuk grouping data vector data_vec
menjadi 1 buah data bertipe NumberCategory
.
- Data numerik genap elemen
data_vec
masuk ke propertyeven
milikNumberCategory
. - Data numerik ganjil elemen
data_vec
masuk ke propertyodd
milikNumberCategory
.
Dalam penerapannnya, parameter pertama method fold
diisi dengan initial value, dan parameter ke-2 isinya closure untuk akumulasi data.
#[derive(Debug)]
struct NumberCategory {
even: Vec<i32>,
odd: Vec<i32>,
}
let data_vec = vec![1, 2, 3, 4];
let data_grouped = data_vec.iter().fold(
NumberCategory{ even: Vec::new(), odd: Vec::new() },
|mut group, each| {
if *each % 2 == 0 {
group.even.push(*each)
} else {
group.odd.push(*each)
}
group
}
);
println!("{:?}", data_grouped);
Pada kode di atas, data each
di-cek nilainya, jika genap maka dimasukan dalam group.even
, dan sisanya masuk group.odd
.
◉ Method inspect
Biasanya digunakan sewaktu debugging, untuk inspeksi flow pemanggilan method-method milik Iterator. Contoh penerapannya:
let data_vec = vec![1, 2, 3, 4];
let result = data_vec.iter()
.cloned()
.inspect(|x| println!("about to filter: {x}"))
.filter(|x| x % 2 == 0)
.inspect(|x| println!("made it through filter: {x}"))
.fold(0, |sum, i| sum + i);
println!("sum: {:?}", result);
// sum: 10
◉ Method sum
Digunakan untuk mencari total/summary data slice numerik.
let data_vec = vec![1.1, 2.2, 3.3, 4.5];
let result: f64 = data_vec.iter().sum();
println!("sum: {:?}", result);
// sum: 11.1
◉ Method reduce
Method ini digunakan untuk mengiterasi setiap element array dengan setiap iterasi menampilkan data element ke-n
dan element ke-n+1
.
Contoh penerapan sederhananya pada pencarian angka numerik terbesar, yang kode-nya bisa dilihat berikut ini:
let data_vec = vec![1, 2, 3, 4];
let max_number = data_vec.iter().reduce(|left, right| {
print!("left ({left}) vs right ({right})");
if *left >= *right {
println!(" -> left ({left}) is greater");
left
} else {
println!(" -> right ({right}) is greater");
right
}
});
match max_number {
Some(n) => println!("max_number: {:?}", n),
None => println!("no data found"),
}
◉ Method min
, max
Method min
digunakan untuk mencari elemen terkecil pada Iterator, dan max
untuk mencari elemen terbesar. Keduanya mengembalikan nilai balik bertipe Option
.
let data_vec = vec![1, 2, 3, 4];
let min = data_vec.iter().min().unwrap();
println!("min: {min}");
// min: 1
let max = data_vec.iter().max().unwrap();
println!("max: {max}");
// max: 4
A.50.4. Method lainnya
◉ Method sort
milik Vec<T>
Method sort
bukanlah property Iterator, melainkan milik tipe data vector. Kegunaan method ini adalah untuk sorting urutan elemen vector.
let mut data_vec = vec![2, 3, 1, 4];
println!("before: {data_vec:?}");
// before: [2, 3, 1, 4]
data_vec.sort();
println!("after: {data_vec:?}");
// after: [1, 2, 3, 4]
Catatan chapter 📑
◉ Source code praktik
github.com/novalagung/dasarpemrogramanrust-example/../trait_iterator
◉ Chapter relevan lainnya
◉ Work in progress
- Desugar iterator