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_arryang bertipe data[i32; 3]dikonversi ke tipe dataIteratormenggunakan methoditer. - Object
Iteratorkemudian di-iterasi menggunakan methodmapdan nilai baliknya dijadikan replacement data elemen tersebut. - Object
Iteratorkemudian 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_vecyang merupakan koleksi string dikonversi menjadi objectIteratormenggunakan methoditer.Kemudian method
mapdiakses. Setiap elemendata_vecdi-iterasi, kemudian dikonversi dariStringkei32, lalu dijadikan replacement data elemen tersebut. Jika proses konversi gagal, maka angka0digunakan 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
filterdengan kondisi*e > 0 && *e % 2 == 0yang kurang lebih artinya, jika nilai dereferenceelebih besar dari0dan 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
collectdengan 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
itermengembalikan dataIteratoryang 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_mutmengembalikan dataIteratoryang 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_itermengkonversi 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
Orderingakan 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
eqdigunakan untuk equal lexicographical comparison - Method
nedigunakan untuk not equal lexicographical comparison - Method
gtdigunakan untuk greater than lexicographical comparison - Method
gedigunakan untuk greater than or equal lexicographical comparison - Method
ltdigunakan untuk lower than lexicographical comparison - Method
ledigunakan 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
foldmilik Rust mirip sepertireducepada 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_vecmasuk ke propertyevenmilikNumberCategory. - Data numerik ganjil elemen
data_vecmasuk ke propertyoddmilikNumberCategory.
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