Satu adapter map mengubah elemen; satu adapter filter menyaring elemen. Pekerjaan data nyata hampir selalu membutuhkan keduanya sekaligus, sering berkali-kali: saring produk yang tersedia, saring lagi yang sesuai anggaran, lalu rangkai sisanya menjadi label tampilan. Kekuatan sebenarnya dari iterator Rust baru terasa ketika adapter-adapter tersebut dirangkai bertingkat membentuk pipeline — rantai tahapan yang setiap tahapnya adalah iterator pembungkus, dan setiap elemen mengalir menembus seluruh rantai satu per satu. Pada chapter ini kita susun pipeline dua tahap, tiga tahap, lalu membuktikan dengan penanda println! bahwa urutan tahap menentukan berapa banyak kerja yang benar-benar dilakukan.
Pipeline Dua Tahap: filter lalu map #
Katalog toko kita hanya ingin menampilkan produk berharga premium (minimal Rp15.000) dalam bentuk label rapi. Kebutuhan ini terpecah menjadi dua tahap deklaratif: filter menyaring produk premium, map merangkai produk yang lolos menjadi label:
struct Product {
name: String,
price: i32,
}
fn main() {
let products = vec![
Product { name: String::from("Kopi Gayo"), price: 15000 },
Product { name: String::from("Teh Melati"), price: 8000 },
Product { name: String::from("Kopi Toraja"), price: 17500 },
];
let premium_labels = products
.iter()
.filter(|product| product.price >= 15000)
.map(|product| format!("{} - Rp{}", product.name, product.price));
for line in premium_labels {
println!("Menu premium: {}", line);
}
}Output yang Dihasilkan:
Menu premium: Kopi Gayo - Rp15000
Menu premium: Kopi Toraja - Rp17500Mari kita bedah baris demi baris:
- Baris 13-16 (
let premium_labels = products.iter()...map(...)): rantai dibaca dari atas ke bawah.products.iter()membuat iterator sumber bertugas menyerahkan∏.filter(...)membungkusnya menjadi iterator penyaring;.map(...)membungkus lagi menjadi iterator pengubah. Variabelpremium_labelspada akhirnya memegang pipeline tiga lapis — bukan hasil akhir, karena semuanya masih lazy. - Baris 15 (
.filter(|product| product.price >= 15000)): predicate pertama memeriksa harga setiap&Productyang ditarik.Teh Melatidengan harga8000berhenti di tahap ini dan tidak pernah sampai kemap. - Baris 16 (
.map(|product| format!("{} - Rp{}", ...))): closure hanya menerima elemen yang lolos saringan — dua dari tiga produk. Setiap yang lolos dirangkai menjadiStringlabel. - Baris 18 (
for line in premium_labels):foradalah consumer yang menyalakan seluruh pipeline. Penanda pentingnya: badan perulangan hanya berjalan dua kali, sebanyak elemen yang lolos, danproductstetap utuh karena seluruh rantai berdiri di atas pinjamaniter().
Aliran satu elemen melewati pipeline dapat kita petakan seperti ini:
Diagram di atas menegaskan dua jalur berbeda: Kopi Toraja mengalir melewati kedua adapter sampai ke for, sedangkan Teh Melati berhenti di filter dan langsung dibuang — closure map tidak pernah melihatnya.
Pipeline Tiga Tahap: filter, filter, map #
Adapter bertingkat tidak berhenti di dua. Kebutuhan berikutnya memuat dua kriteria berurutan: menu hemat hanya menampilkan produk yang stoknya tersedia dan harganya tidak melebihi Rp15.000. Kriteria ganda semacam ini paling jelas ditulis sebagai dua tahap filter terpisah:
struct Product {
name: String,
price: i32,
stock: i32,
}
fn main() {
let products = vec![
Product { name: String::from("Kopi Gayo"), price: 15000, stock: 12 },
Product { name: String::from("Teh Melati"), price: 8000, stock: 0 },
Product { name: String::from("Gula Aren"), price: 5000, stock: 20 },
Product { name: String::from("Kopi Toraja"), price: 17500, stock: 3 },
];
let budget_menu = products
.iter()
.filter(|product| product.stock > 0)
.filter(|product| product.price <= 15000)
.map(|product| format!("{} - Rp{}", product.name, product.price));
for line in budget_menu {
println!("Menu hemat: {}", line);
}
}Output yang Dihasilkan:
Menu hemat: Kopi Gayo - Rp15000
Menu hemat: Gula Aren - Rp5000Uraian baris-per-baris:
- Baris 15 (
let budget_menu = products): kebiasaan penulisan pipeline berdiri sendiri — sumber di baris pertama, setiap adapter di baris berikutnya — membuat rantai tahap terbaca seperti resep dari atas ke bawah. - Baris 17 (
.filter(|product| product.stock > 0)): penyaringan pertama membuangTeh Melatikarena stoknya0. Tiga produk lanjut ke tahap kedua. - Baris 18 (
.filter(|product| product.price <= 15000)): penyaringan kedua membuangKopi Torajakarena harganya17500. Dua produk tersisa. - Baris 19 (
.map(...)): transformasi akhir merangkai dua produk yang bertahan menjadi label. Jumlah kerjamapmengikuti jumlah elemen yang lolos — dua kali, bukan empat kali.
Perhatikan bahwa dua tahap filter berurutan bukan tanda keraguan: setiap filter adalah iterator pembungkus tersendiri dengan predicate sendiri. Membaca pipeline baris demi baris seperti membaca daftar syarat berurutan jauh lebih mudah diaudit daripada satu predicate raksasa berisi && bertingkat. Versi imperatif dari kebutuhan yang sama — perulangan for dengan if stok > 0 bersarang di dalamnya, lalu if harga <= 15000 bersarang lagi, lalu push hasil format ke Vec sementara — mengharuskan pembaca melacak tiga tingkat indentasi untuk mengetahui syarat mana yang memotong aliran data. Pipeline membalik arah baca tersebut: setiap tahap menempati satu baris, syarat berjalan dari atas ke bawah, dan satu tahap dapat ditambah atau dihapus tanpa menyentuh tahap lainnya. Itulah makna kata deklaratif yang melekat pada pendekatan iterator — kita menyatakan tahapan apa yang harus terjadi, bukan bagaimana perulangan mengatur jalannya.
Urutan Tahap Menentukan Kerja #
Pipeline yang sama hasil akhirnya bisa tersusun dengan urutan berbeda, dan urutan menentukan kerja. Kita buktikan dengan penanda println! di dalam closure map. Versi A menyaring dulu, mengubah kemudian:
fn main() {
let prices = vec![12000, 15000, 17500, 20000];
println!("Urutan A: filter dulu, map kemudian");
for label in prices
.into_iter()
.filter(|price| *price > 15000)
.map(|price| {
println!(" map dijalankan untuk elemen {}", price);
format!("Rp{}", price)
})
{
println!("Hasil: {}", label);
}
}Output yang Dihasilkan:
Urutan A: filter dulu, map kemudian
map dijalankan untuk elemen 17500
Hasil: Rp17500
map dijalankan untuk elemen 20000
Hasil: Rp20000Versi B membalik urutannya — mengubah dulu, menyaring kemudian:
fn main() {
let prices = vec![12000, 15000, 17500, 20000];
println!("Urutan B: map dulu, filter kemudian");
for label in prices
.into_iter()
.map(|price| {
println!(" map dijalankan untuk elemen {}", price);
format!("Rp{}", price)
})
.filter(|label| label != "Rp12000" && label != "Rp15000")
{
println!("Hasil: {}", label);
}
}Output yang Dihasilkan:
Urutan B: map dulu, filter kemudian
map dijalankan untuk elemen 12000
map dijalankan untuk elemen 15000
map dijalankan untuk elemen 17500
Hasil: Rp17500
map dijalankan untuk elemen 20000
Hasil: Rp20000Mari kita bandingkan kedua output dengan cermat:
- Versi A (filter dulu): penanda
map dijalankanhanya muncul dua kali — untuk17500dan20000. Elemen12000dan15000sudah terbuang sebelum sampai kemap, sehingga transformasi tidak pernah menyentuhnya. - Versi B (map dulu): penanda muncul empat kali. Semua elemen diubah menjadi
Stringlebih dulu, barufiltermembuang dua label yang tidak diinginkan. Hasil akhir sama-sama dua elemen, tetapi closuremapbekerja dua kali lebih banyak. - Implikasi praktis: semakin mahal transformasi di
map— misalnya membangunStringpanjang, menghitung rumus berat, memanggilformat!— semakin besar pemborosan versi B. Aturan praktisnya sederhana: dorongfilterke posisi paling depan yang memungkinkan, sehingga tahap-tahap mahal hanya menerima elemen yang benar-benar dibutuhkan.
Ringkasan #
Adapter dirangkai dengan pemanggilan berantai: setiap filter dan map membungkus iterator sebelumnya menjadi tahap baru, dan setiap elemen mengalir menembus seluruh rantai satu per satu mengikuti model tarik dari chapter Lazy Evaluation. Pipeline dua tahap filter lalu map menjawab kebutuhan “saring lalu format”, pipeline tiga tahap memperlihatkan bahwa dua filter berurutan berfungsi seperti daftar syarat yang mudah diaudit, dan eksperimen urutan A/B membuktikan bahwa filter di depan membuat closure map hanya berjalan pada elemen yang lolos — separuh kerja versi terbaliknya. Kita kini bisa membangun pipeline deklaratif yang presisi; yang tersisa adalah mengeluarkan hasilnya dari aliran menjadi data yang bisa disimpan.