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:
fn map<B, F>(self, f: F) -> Map<Self, F>
where
F: FnMut(Self::Item) -> B,Mari kita baca tanda tangan ini sebagai kontrak:
- Baris 1 (
fn map<B, F>(self, f: F) -> Map<Self, F>):mapmengambilselfberdasarkan nilai — iterator yang dipanggilmapakan dimiliki penuh oleh iterator hasil, dan tipe hasilnya adalahMap<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. - Baris 3 (
F: FnMut(Self::Item) -> B): parameter closurefharus bertipeFnMut— closure yang bisa dipanggil berkali-kali dan boleh mengubah state yang ditangkapnya. Closure menerima satu elemen bertipeSelf::Itemdan mengembalikan nilai bertipeB. TipeBbebas: boleh sama denganItem, boleh pula berbeda total. - Huruf
Bpadamap<B, F>: posisi ini menyatakan bahwaBadalah 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:
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:
Label: KOPI GAYO
Label: KOPI TORAJA
Label: KOPI FLORESMari kita bedah baris demi baris:
- Baris 2 (
let menu = vec!["kopi gayo", "kopi toraja", "kopi flores"];): sumber data berupaVec<&str>berisi tiga nama minuman dalam huruf kecil. - 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&strdan mengembalikanStringhasilto_uppercase(). Perulanganforberperan sebagai consumer yang menarik setiap elemen. - Baris 5 (
println!("Label: {}", label);):labelbertipeString— nilai buatan baru hasil closure — bukan lagi&strasli dari koleksi. Koleksimenusendiri tidak pernah diubah; semua nilai kapital adalah objekStringyang lahir darito_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:
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:
Harga final: Rp16500
Harga final: Rp19250
Harga final: Rp22000Uraian baris-per-baris:
- 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. - Baris 5 (
for labeled in prices.iter().map(|price| {): iterator sumber menyerahkan&i32per elemen; closure menerima nilai tersebut pada parameterprice. Badan closure kini multi-baris, ditandai kurung kurawal pembuka. - Baris 6 (
let price_with_tax = price + (price * tax_rate / 100);): hitungan pajak dilakukan di dalam closure. Nilaipricebertipe&i32, tetapi operator aritmetika pada&i32langsung bekerja berkat dereference otomatis pada operand. - Baris 7 (
format!("Rp{}", price_with_tax)): baris terakhir tanpa titik koma adalah nilai kembalian closure — sebuahStringrapi seperti “Rp16500”. Iteratormapbertugas menyerahkanStringini kepadafor. - Baris 9 (
println!("Harga final: {}", labeled);): badanformenerima label jadi. Tidak adapush, tidak adaVecsementara, 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:
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:
Kopi Gayo x2 unit
Teh Melati x3 unitMari kita bedah bagian pentingnya:
- Baris 7-10 (
let orders = vec![...]): koleksi berisi duaOrder—orderspemiliknya, iterator nanti hanya meminjam lewatiter(). - Baris 12 (
for receipt in orders.iter().map(|order| {): iterator sumber menyerahkan&Orderper elemen. Closure menerima reference tersebut dan membaca kedua field-nya tanpa memindahkan kepemilikanStringdi dalamproduct. - Baris 13 (
format!("{} x{} unit", order.product, order.quantity)):format!membaca field lewat pinjaman dan membangunStringbaru — sehingga setelah perulangan selesai, koleksiorderstetap 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.