Sejak Chapter pertama Section Closure kita membangun fondasi: sintaks pipe, penangkapan environment, tiga trait pemanggilan, move, lalu closure sebagai parameter dan nilai kembalian. Semua fondasi itu bermuara ke satu tempat pemakaian yang paling sering kita jumpai di kode Rust sehari-hari: iterator. Metode seperti map, filter, dan sort_by_key menerima closure dari kita — aturan transformasi, syarat seleksi, kunci pengurutan — lalu menerapkannya pada setiap elemen data. Chapter ini adalah jembatan: kita melihat closure bekerja di atas tiga adapter iterator paling populer dengan data sederhana, tanpa menyelami mekanisme iterator itu sendiri. Mekanisme lengkapnya — trait Iterator, lazy evaluation, dan keluarga adapter yang jauh lebih besar — menunggu kita di Section Iterator yang datang tepat setelah ini.

Masalah: Transformasi Data Tanpa Loop Manual #

Kita mulai dari masalah yang pasti akrab. Data harga menu tersimpan dalam array, dan kita perlu menaikkan seluruh harga sebesar Rp2.000 untuk katalog baru. Dengan peralatan sejauh ini, solusinya adalah loop manual: buat Vec kosong, ulangi setiap elemen, hitung nilai baru, lalu push hasilnya:

rust
fn main() {
    let harga_jus = [18000, 22000, 25000];
    let mut harga_baru = Vec::new();

    for harga in harga_jus.iter() {
        harga_baru.push(harga + 2000);
    }

    println!("Harga baru: {:?}", harga_baru);
}

Output yang Dihasilkan:

text
Harga baru: [20000, 24000, 27000]

Kode ini benar, tetapi tiga baris mekanis — wadah sementara, loop, push — mengaburkan niat sebenarnya: naikkan setiap harga sebesar 2.000. Adapter map memindahkan niat itu ke satu baris.

Adapter map #

Metode map menerima closure dan mengembalikan iterator baru yang menerapkan closure pada setiap elemen:

rust
fn main() {
    let harga_jus = [18000, 22000, 25000];
    let harga_baru: Vec<u64> = harga_jus.iter().map(|harga| harga + 2000).collect();

    println!("Harga lama: {:?}", harga_jus);
    println!("Harga baru: {:?}", harga_baru);
}

Output yang Dihasilkan:

text
Harga lama: [18000, 22000, 25000]
Harga baru: [20000, 24000, 27000]

Hasilnya identik dengan loop manual — harga_baru berisi tiga harga yang sudah dinaikkan. Mari kita bedah versi map:

  1. Baris 3 (harga_jus.iter()): pemanggilan iter() meminjam array sebagai iterator yang memuntahkan elemen satu per satu dalam bentuk reference &u64 — pola pinjaman yang sudah kita kenal, array aslinya tetap utuh dan tercetak di baris berikutnya.
  2. Baris 3 (.map(|harga| harga + 2000)): closure |harga| harga + 2000 menjadi aturan transformasi. Parameter harga menerima setiap &u64 dari iterator, dan ekspresi harga + 2000 menghasilkan u64 baru — closure ini tidak menangkap apa pun, persis closure Fn dari Chapter Fn, FnMut, FnOnce.
  3. Baris 3 (.collect()): collect() menjalankan seluruh pipeline dan mengumpulkan hasilnya menjadi koleksi. Anotasi Vec<u64> di sisi kiri memberitahu collect bentuk koleksi yang kita inginkan — mekanisme penuhnya ada di Section Iterator.
  4. Perbandingan dengan loop manual: tiga baris mekanis menyusut jadi satu rantai. Wadah mut, loop for, dan push menghilang; yang tersisa hanya deklarasi data dan aturan transformasinya.

Adapter filter dan Rantai Adapter #

filter menerima closure yang mengembalikan bool sebagai syarat seleksi: elemen dengan hasil true lolos, sisanya dibuang. Kekuatan sebenarnya muncul saat adapter dirantai, dan di sinilah kemampuan menangkap environment kita terpakai — closure filter berikut memakai batas dari scope sekitarnya:

rust
fn main() {
    let harga_menu = [18000, 35000, 12000, 42000, 26000];
    let batas = 25000;

    let menu_terjangkau: Vec<u64> = harga_menu
        .iter()
        .filter(|harga| **harga <= batas)
        .map(|harga| harga + 1000)
        .collect();

    println!("Menu di bawah Rp{}: {:?}", batas, menu_terjangkau);
}

Output yang Dihasilkan:

text
Menu di bawah Rp25000: [19000, 13000]

Mari kita bedah rantai ini:

  1. Baris 7 (.filter(|harga| **harga <= batas)): closure filter menerima tiap elemen dan mengembalikan bool. Perhatikan dua tanda bintang pada **harga: iter() memuntahkan &u64, lalu filter membungkus elemennya sekali lagi menjadi &&u64 karena filter butuh memeriksa hasil bool sebelum memutuskan nasib elemen — tanda bintang pertama melepas bungkus filter, tanda bintang kedua melepas reference iter().
  2. Baris 7 (penangkapan batas): closure membandingkan **harga dengan batas — variabel dari scope main yang ditangkap sebagai reference immutable. Inilah closure bertugas sungguhan: syarat seleksinya tidak dikirim sebagai argumen, melainkan dibawa dari lingkungan sekitar. Mengganti batas di satu tempat mengubah perilaku seluruh rantai.
  3. Baris 8 (.map(|harga| harga + 1000)): elemen yang lolos seleksi filter adalah 18000 dan 12000 — 26000 gagal karena melebihi batas. Adapter map berikutnya menaikkan masing-masing sebesar 1000 menjadi 19000 dan 13000.
  4. Baris 5–9 (urutan eksekusi): rantai dibaca dari atas ke bawah — saring dulu, transformasi belakangan. Urutan ini menentukan hasil: membalik filter dan map akan menaikkan seluruh harga dulu sehingga 24000 lolos seleksi. Menyusun urutan adapter adalah keputusan desain, bukan sekadar gaya penulisan.

sort_by_key dan Kerabat Lainnya #

Keluarga adapter iterator berjalan juga di dalam metode koleksi. sort_by_key pada Vec menerima closure yang menentukan kunci pengurutan — closure mengembalikan nilai yang bisa dibandingkan, dan elemen diurutkan berdasarkan nilai tersebut:

rust
fn main() {
    let mut pesanan = vec![
        ("Siti Rahma", 7),
        ("Budi Santoso", 1),
        ("Rina Wijaya", 4),
    ];

    pesanan.sort_by_key(|item| item.1);
    println!("{:?}", pesanan);
}

Output yang Dihasilkan:

text
[("Budi Santoso", 1), ("Rina Wijaya", 4), ("Siti Rahma", 7)]

Mari kita bedah:

  1. Baris 2–6 (let mut pesanan = vec![...]): Vec berisi tuple (&str, u64) — nama pelanggan dan jumlah item pesanannya. Vec dan vec! sudah kita kenal di Section Koleksi; mut dibutuhkan karena sort_by_key mengatur ulang isi di tempat.
  2. Baris 8 (pesanan.sort_by_key(|item| item.1)): closure |item| item.1 menerima tiap tuple dan mengembalikan item.1 — angka jumlah item — sebagai kunci urutan. Hasilnya: pesanan tersusun dari jumlah terkecil ke terbesar. Ganti kuncinya menjadi item.0 dan urutan berubah menjadi alfabetis berdasarkan nama.
  3. Baris 9 (println!("{:?}", pesanan);): tuple mengimplementasikan Debug, sehingga format {:?} mencetak seluruh isi Vec — termasuk tanda kutip dan kurung yang terlihat pada output.

Polanya sama di seluruh pustaka standar: metode yang “butuh pendapat kita” meminta pendapat itu lewat closure. retain menyaring isi koleksi di tempat dengan closure bool, find mengembalikan elemen pertama yang cocok dengan closure, dan keluarga besar adapter iterator lainnya menanti di Section Iterator.

Chapter ini sengaja menahan diri pada tiga pemakaian closure yang paling umum. Di balik map dan filter ada satu trait Iterator dengan satu method wajib next; adapter hanyalah method yang membangun iterator baru di atas iterator lama; dan lazy evaluation membuat rantai panjang tetap hemat karena elemen diproses satu per satu tanpa koleksi perantara. Semua mekanisme itu, ditambah adapter seperti fold, zip, enumerate, dan take, adalah wilayah Section Iterator — dan kita masuki wilayah itu dengan bekal lengkap: closure di tangan, trait di kepala.

Ringkasan #

Closure menemukan panggung terbesarnya di iterator: map menerima aturan transformasi per elemen, filter menerima syarat seleksi bool — seraya menangkap variabel seperti batas dari lingkungan sekitar — dan sort_by_key menerima kunci pengurutan. Rantai adapter menggantikan loop manual for + push dengan deklarasi berurutan yang terbaca seperti niatnya, dan collect() memicu eksekusi seluruh rantai. Section berikutnya membuka mesin di balik semua ini: trait Iterator, lazy evaluation, dan keluarga adapter yang jauh lebih besar dari yang kita intip di sini.