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:

rust
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:

text
Menu premium: Kopi Gayo - Rp15000
Menu premium: Kopi Toraja - Rp17500

Mari kita bedah baris demi baris:

  1. 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. Variabel premium_labels pada akhirnya memegang pipeline tiga lapis — bukan hasil akhir, karena semuanya masih lazy.
  2. Baris 15 (.filter(|product| product.price >= 15000)): predicate pertama memeriksa harga setiap &Product yang ditarik. Teh Melati dengan harga 8000 berhenti di tahap ini dan tidak pernah sampai ke map.
  3. Baris 16 (.map(|product| format!("{} - Rp{}", ...))): closure hanya menerima elemen yang lolos saringan — dua dari tiga produk. Setiap yang lolos dirangkai menjadi String label.
  4. Baris 18 (for line in premium_labels): for adalah consumer yang menyalakan seluruh pipeline. Penanda pentingnya: badan perulangan hanya berjalan dua kali, sebanyak elemen yang lolos, dan products tetap utuh karena seluruh rantai berdiri di atas pinjaman iter().

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:

rust
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:

text
Menu hemat: Kopi Gayo - Rp15000
Menu hemat: Gula Aren - Rp5000

Uraian baris-per-baris:

  1. 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.
  2. Baris 17 (.filter(|product| product.stock > 0)): penyaringan pertama membuang Teh Melati karena stoknya 0. Tiga produk lanjut ke tahap kedua.
  3. Baris 18 (.filter(|product| product.price <= 15000)): penyaringan kedua membuang Kopi Toraja karena harganya 17500. Dua produk tersisa.
  4. Baris 19 (.map(...)): transformasi akhir merangkai dua produk yang bertahan menjadi label. Jumlah kerja map mengikuti 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:

rust
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:

text
Urutan A: filter dulu, map kemudian
  map dijalankan untuk elemen 17500
Hasil: Rp17500
  map dijalankan untuk elemen 20000
Hasil: Rp20000

Versi B membalik urutannya — mengubah dulu, menyaring kemudian:

rust
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:

text
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: Rp20000

Mari kita bandingkan kedua output dengan cermat:

  1. Versi A (filter dulu): penanda map dijalankan hanya muncul dua kali — untuk 17500 dan 20000. Elemen 12000 dan 15000 sudah terbuang sebelum sampai ke map, sehingga transformasi tidak pernah menyentuhnya.
  2. Versi B (map dulu): penanda muncul empat kali. Semua elemen diubah menjadi String lebih dulu, baru filter membuang dua label yang tidak diinginkan. Hasil akhir sama-sama dua elemen, tetapi closure map bekerja dua kali lebih banyak.
  3. Implikasi praktis: semakin mahal transformasi di map — misalnya membangun String panjang, menghitung rumus berat, memanggil format! — semakin besar pemborosan versi B. Aturan praktisnya sederhana: dorong filter ke 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.