Transformasi data adalah pekerjaan paling khas dalam pengolahan koleksi: ubah seluruh nama menu menjadi huruf kapital, tambahkan pajak ke setiap harga, atau rangkap setiap pesanan menjadi baris struk. Cara lama yang kita kenal — perulangan manual, koleksi hasil baru, push satu per satu — berfungsi, tetapi mengharuskan kita menulis mesin pengulangannya setiap kali. Iterator Rust menawarkan bentuk yang lebih deklaratif lewat adapter map: kita cukup menyatakan “ubah setiap elemen dengan closure ini”, dan mekanisme penarikan yang sudah kita pahami dari chapter-chapter sebelumnya bekerja sendiri. Chapter ini membedah map sampai ke tanda tangannya, cara kerjanya yang tetap lazy, dan kemampuannya mengubah tipe elemen di tengah jalan.

Masalah Pola Push Manual #

Mari kita nyatakan masalahnya dengan jelas. Untuk mengubah tiga nama menu menjadi label huruf kapital, pola manual yang selama ini kita tulis membutuhkan tiga bahan: variabel labels bertipe Vec<String> yang dibuat kosong, perulangan for yang membaca koleksi sumber, dan pemanggilan push di setiap putaran. Kode seperti itu menumpuk tiga keputusan teknis sekaligus — bagaimana menyimpan hasil, kapan menambah hasil, bagaimana berpindah elemen — padahal niat kita sesungguhnya hanya satu: setiap nama menjadi huruf kapital. Adapter map memungkinkan kita menuliskan niat tersebut secara langsung, tanpa mengurus mesin pengulangannya.

Adapter map sebagai Pembungkus #

map adalah adapter: method bawaan pada trait Iterator yang menerima sebuah iterator lalu mengembalikan iterator baru. Signature aslinya di pustaka standar tampak seperti ini:

rust
fn map<B, F>(self, f: F) -> Map<Self, F>
where
    F: FnMut(Self::Item) -> B,

Mari kita baca tanda tangan ini sebagai kontrak:

  1. Baris 1 (fn map<B, F>(self, f: F) -> Map<Self, F>): map mengambil self berdasarkan nilai — iterator yang dipanggil map akan dimiliki penuh oleh iterator hasil, dan tipe hasilnya adalah Map<Self, F>, struct pembungkus generik dari pustaka standar. Perlu dicermati: yang dipindahkan adalah iterator-nya, bukan data koleksi di belakangnya; pinjaman terhadap koleksi tetap hidup di dalam pembungkus.
  2. Baris 3 (F: FnMut(Self::Item) -> B): parameter closure f harus bertipe FnMut — closure yang bisa dipanggil berkali-kali dan boleh mengubah state yang ditangkapnya. Closure menerima satu elemen bertipe Self::Item dan mengembalikan nilai bertipe B. Tipe B bebas: boleh sama dengan Item, boleh pula berbeda total.
  3. Huruf B pada map<B, F>: posisi ini menyatakan bahwa B adalah tipe yang akan disimpulkan dari isi closure — teknik generik yang sudah kita kenal dari section Generics.

Karena map hanya membungkus, hasilnya adalah iterator yang masih lazy persis seperti pokok bahasan chapter sebelumnya: tidak ada satu elemen pun diolah sampai consumer datang menarik.

Contoh Dasar: Kapitalisasi Nama Menu #

Bentuk paling sederhana dari map mengubah setiap elemen menjadi nilai bertipe yang sama:

rust
fn main() {
    let menu = vec!["kopi gayo", "kopi toraja", "kopi flores"];

    for label in menu.iter().map(|name| name.to_uppercase()) {
        println!("Label: {}", label);
    }
}

Output yang Dihasilkan:

text
Label: KOPI GAYO
Label: KOPI TORAJA
Label: KOPI FLORES

Mari kita bedah baris demi baris:

  1. Baris 2 (let menu = vec!["kopi gayo", "kopi toraja", "kopi flores"];): sumber data berupa Vec<&str> berisi tiga nama minuman dalam huruf kecil.
  2. Baris 4 (for label in menu.iter().map(|name| name.to_uppercase())): rantai dibaca dari kiri ke kanan — menu.iter() menghasilkan iterator bertugas menyerahkan &str, lalu .map(...) membungkusnya menjadi iterator baru. Closure |name| name.to_uppercase() menerima &str dan mengembalikan String hasil to_uppercase(). Perulangan for berperan sebagai consumer yang menarik setiap elemen.
  3. Baris 5 (println!("Label: {}", label);): label bertipe String — nilai buatan baru hasil closure — bukan lagi &str asli dari koleksi. Koleksi menu sendiri tidak pernah diubah; semua nilai kapital adalah objek String yang lahir dari to_uppercase().

Perhatikan sesuatu yang penting dari poin terakhir: elemen masuk sebagai &str, keluar sebagai String. map boleh mengubah tipe elemen, dan contoh kita baru saja melakukannya.

Mengubah Angka Menjadi Label Berformat #

Tipe kembalian closure tidak harus String dari string — closure dapat berisi hitungan multi-baris. Toko kita perlu mencetak harga final yang sudah termasuk pajak 10 persen, lengkap dengan awalan mata uang:

rust
fn main() {
    let prices = vec![15000, 17500, 20000];
    let tax_rate = 10;

    for labeled in prices.iter().map(|price| {
        let price_with_tax = price + (price * tax_rate / 100);
        format!("Rp{}", price_with_tax)
    }) {
        println!("Harga final: {}", labeled);
    }
}

Output yang Dihasilkan:

text
Harga final: Rp16500
Harga final: Rp19250
Harga final: Rp22000

Uraian baris-per-baris:

  1. Baris 3 (let tax_rate = 10;): variabel di luar closure akan ditangkap (captured) oleh closure — teknik dari section Closure yang di sini membuat tarif pajak tinggal satu tempat untuk diubah.
  2. Baris 5 (for labeled in prices.iter().map(|price| {): iterator sumber menyerahkan &i32 per elemen; closure menerima nilai tersebut pada parameter price. Badan closure kini multi-baris, ditandai kurung kurawal pembuka.
  3. Baris 6 (let price_with_tax = price + (price * tax_rate / 100);): hitungan pajak dilakukan di dalam closure. Nilai price bertipe &i32, tetapi operator aritmetika pada &i32 langsung bekerja berkat dereference otomatis pada operand.
  4. Baris 7 (format!("Rp{}", price_with_tax)): baris terakhir tanpa titik koma adalah nilai kembalian closure — sebuah String rapi seperti “Rp16500”. Iterator map bertugas menyerahkan String ini kepada for.
  5. Baris 9 (println!("Harga final: {}", labeled);): badan for menerima label jadi. Tidak ada push, tidak ada Vec sementara, tidak ada variabel status.

Contoh ini memperlihatkan pergeseran tipe yang lebih dramatis dari contoh pertama: elemen masuk sebagai &i32 (angka pinjaman), keluar sebagai String (teks berformat). Iterator Map menjadi penjaga tipe perantara yang menghubungkan keduanya.

map pada Koleksi Struct #

Bentuk ketiga melengkapi gambar: map juga lazim dipakai untuk merangkai field struct menjadi data siap tampil. Kasir toko kita ingin mencetak struk ringkas dari daftar Order:

rust
struct Order {
    product: String,
    quantity: i32,
}

fn main() {
    let orders = vec![
        Order { product: String::from("Kopi Gayo"), quantity: 2 },
        Order { product: String::from("Teh Melati"), quantity: 3 },
    ];

    for receipt in orders.iter().map(|order| {
        format!("{} x{} unit", order.product, order.quantity)
    }) {
        println!("{}", receipt);
    }
}

Output yang Dihasilkan:

text
Kopi Gayo x2 unit
Teh Melati x3 unit

Mari kita bedah bagian pentingnya:

  1. Baris 7-10 (let orders = vec![...]): koleksi berisi dua Order — orders pemiliknya, iterator nanti hanya meminjam lewat iter().
  2. Baris 12 (for receipt in orders.iter().map(|order| {): iterator sumber menyerahkan &Order per elemen. Closure menerima reference tersebut dan membaca kedua field-nya tanpa memindahkan kepemilikan String di dalam product.
  3. Baris 13 (format!("{} x{} unit", order.product, order.quantity)): format! membaca field lewat pinjaman dan membangun String baru — sehingga setelah perulangan selesai, koleksi orders tetap utuh dan dapat dipakai lagi.

Ringkasan #

Adapter map membungkus sebuah iterator menjadi iterator baru yang menjalankan closure pada setiap elemen yang ditarik: signature fn map<B, F>(self, f: F) -> Map<Self, F> menegaskan bahwa iterator sumber dimiliki pembungkusnya, closure bertipe FnMut(Self::Item) -> B, dan tipe hasil B boleh berbeda dari tipe sumber — seperti &i32 yang menjadi String pada contoh harga berpajak. Seluruh transformasi tetap lazy sampai consumer menarik, koleksi sumber tidak pernah berubah, dan tidak ada Vec perantara yang kita kelola manual. Dengan satu adapter transformasi di tangan, kebutuhan berikutnya adalah menyaring elemen sebelum diubah — tugas yang dikerjakan adapter filter pada chapter berikutnya.