Closure adalah fungsi kecil tanpa nama yang bisa memakai variabel di sekitarnya, dan iterator adalah cara mengambil isi sebuah koleksi satu per satu. Digabung, keduanya mengolah data dalam satu rantai: saring, ubah, jumlahkan. Bayangin dapur Kedai Martabak Bang Rusdi di akhir hari. Bang Rusdi menulis kartu instruksi kecil, "potong 10% untuk pesanan di atas 50.000", lalu seorang petugas mencabut nota dari paku satu per satu. Tiap nota lewat pos sortir, lalu pos potong harga, lalu sampai di kasir yang menjumlah. Kartu instruksi itu closure, petugas yang mencabut satu per satu itu iterator, pos-posnya adapter (filter, map), dan kasir di ujung itu consumer (sum). Kalau kasirnya gak ada, petugasnya gak mulai mencabut nota sama sekali.
🧪 Tebakan cepat: program ini menulis
pesanan.iter().map(|p| { println!("cek {}", p); p * 2 });laluprintln!("selesai");. Apa yang tercetak? Tebak dulu, jawabannya ada di bagian "Lazy: gak ada yang jalan tanpa consumer".
Closure: fungsi kecil tanpa nama
fn main() {
let diskon = 10;
let potong = |harga: u32| harga - harga * diskon / 100;
let tambah_ongkir = |total| total + 10000;
println!("{}", potong(102000));
println!("{}", tambah_ongkir(91800));
println!("Diskon tetap {}%", diskon);
}
91800
101800
Diskon tetap 10%
- Parameter closure ditulis di antara dua garis tegak,
|harga: u32|, lalu isinya. Isi satu ekspresi gak butuh kurung kurawal; isi beberapa baris ditulis|p| { ... }. - Tipe parameternya boleh ditebak compiler, seperti
|total|, dari pemakaian pertamanya. potongmemakaidiskon, variabel milikmain. Ini disebut capture (menangkap). Fungsi biasa (fn) gak bisa:fn potongdi dalammainyang memakaidiskonditolak dengan E0434can't capture dynamic environment in a fn item.- Closure disimpan di variabel dan dipanggil seperti fungsi:
potong(102000)memberi 91800.
Tiga cara menangkap: Fn, FnMut, FnOnce
Compiler melihat apa yang isi closure lakukan pada variabel yang ditangkapnya, lalu memberinya salah satu trait ini:
| Trait | Isi closure terhadap variabel luar | Boleh dipanggil | Contoh |
|---|---|---|---|
Fn |
cuma membaca (pinjam &) |
berkali-kali | |h| h - h * diskon / 100 |
FnMut |
mengubah (pinjam &mut) |
berkali-kali, closure-nya let mut |
|h| omzet += h |
FnOnce |
menyerahkan miliknya ke luar (move) | sekali | || nota |
fn main() {
let mut omzet: u32 = 0;
let mut catat = |harga: u32| omzet += harga;
catat(102000);
catat(70000);
println!("Omzet: {}", omzet);
let nota = String::from("2x martabak manis");
let serahkan = || nota;
let dapur = serahkan();
println!("Dapur: {}", dapur);
}
Omzet: 172000
Dapur: 2x martabak manis
catatmengubahomzet, jadi iaFnMutdan haruslet mut catat. Tanpamut: E0596cannot borrow `catat` as mutable. Selamacatatmasih akan dipanggil, ia memegang&mut omzet, jadi mencetakomzetdi antara duacatat(...)kena E0502, aturan dari pelajaran borrowing.serahkanmengembalikannotaitu sendiri, jadi iaFnOnce. Memanggilnya kedua kali ditolak:closure cannot be invoked more than once because it moves the variable `nota` out of its environment.
Kata kunci move di depan closure mengatur cara menangkap: closure mengambil pemilik variabelnya, bukan meminjam. Tapi trait-nya tetap ditentukan isinya:
let teks = String::from("2x martabak manis");
let cetak = move || println!("{}", teks);
cetak();
cetak();
Ini mencetak 2x martabak manis dua kali. teks dimiliki closure, tapi isinya cuma membaca, jadi cetak tetap Fn dan boleh dipanggil berkali-kali. Yang hilang adalah teks di main: memakainya sesudah baris itu memberi E0382 borrow of moved value: `teks` , value moved into closure here.
move dan thread, sekilas
Thread adalah jalur kerja yang berjalan bersamaan, seperti dapur yang memasak sambil kasir melayani. thread::spawn menjalankan closure di thread baru:
use std::thread;
fn main() {
let nota = String::from("2x martabak manis");
let dapur = thread::spawn(move || {
println!("Dapur memasak: {}", nota);
});
dapur.join().unwrap();
}
Output-nya Dapur memasak: 2x martabak manis. Tanpa move, ditolak dengan E0373 closure may outlive the current function, but it borrows `nota`, which is owned by the current function: thread-nya bisa hidup lebih lama dari main, jadi pinjaman gak cukup. move menyerahkan nota ke thread itu. join() menunggu thread-nya selesai. Thread dibahas lebih jauh di pelajaran smart pointer.
iter, iter_mut, dan into_iter
fn main() {
let mut pesanan = vec![102000, 70000, 38000];
for p in pesanan.iter() {
println!("baca {}", p);
}
for p in pesanan.iter_mut() {
*p += 2000;
}
println!("{:?}", pesanan);
for p in pesanan.into_iter() {
println!("ambil {}", p);
}
}
baca 102000
baca 70000
baca 38000
[104000, 72000, 40000]
ambil 104000
ambil 72000
ambil 40000
iter()meminjam tiap isi untuk dibaca (&u32). Vec-nya tetap bisa dipakai sesudahnya. Sama denganfor p in &pesanan.iter_mut()meminjam tiap isi untuk diubah (&mut u32), jadi*p += 2000menambah ongkos bungkus 2000 ke tiap pesanan. Sama denganfor p in &mut pesanan.into_iter()mengambil Vec-nya dan memberi tiap isi sebagai milik (u32). Sesudah itupesanangak valid:pesanan.len()ditolak dengan E0382, karena`into_iter` takes ownership of the receiver `self`. Sama denganfor p in pesanan.
Adapter dan consumer
Adapter mengubah iterator jadi iterator lain: map (ubah tiap isi), filter (saring), enumerate (beri nomor), zip (pasangkan dengan iterator lain). Consumer menarik isinya sampai habis dan memberi hasil: sum, count, fold, collect, atau for.
fn main() {
let pesanan: Vec<u32> = vec![102000, 70000, 38000];
let pelanggan = ["Sari", "Budi", "Rina"];
let ribuan: Vec<u32> = pesanan.iter().map(|p| p / 1000).collect();
let besar = pesanan.iter().filter(|&&p| p >= 50000).count();
let total: u32 = pesanan.iter().sum();
let total_fold = pesanan.iter().fold(0, |acc, p| acc + p);
println!("{:?}", ribuan);
println!("{} pesanan besar", besar);
println!("{} {}", total, total_fold);
for (i, p) in pesanan.iter().enumerate() {
println!("{}. {}", i + 1, p);
}
for (nama, p) in pelanggan.iter().zip(pesanan.iter()) {
println!("{}: {}", nama, p);
}
}
[102, 70, 38]
2 pesanan besar
210000 210000
1. 102000
2. 70000
3. 38000
Sari: 102000
Budi: 70000
Rina: 38000
collect()mengumpulkan hasilnya jadi koleksi, dan tipenya harus ditulis:let ribuan: Vec<u32>. Tanpa tipe: E0283type annotations needed.filtermemberi closure-nya pinjaman ke tiap isi, dan isi dariiter()sudah pinjaman, jadipdi sana&&u32. Pola|&&p|membuka dua-duanya, jadip >= 50000bisa langsung.|p| p >= 50000ditolak:expected `&&_`, found integer. Cara lain:|p| **p >= 50000, atau|x| *x % 2 == 0, karena%juga bisa dipakai pada&i32.fold(0, |acc, p| acc + p)mulai dari 0 dan menyimpan hasil sementara diacc.sumadalahfolduntuk penjumlahan.enumeratememberi pasangan (nomor, isi) mulai dari 0.zipmemasangkan isi ke-0 dengan ke-0, ke-1 dengan ke-1, dan berhenti di yang lebih pendek.
Lazy: gak ada yang jalan tanpa consumer
Ini kode tebakan tadi:
fn main() {
let pesanan = vec![102000, 70000, 38000];
pesanan.iter().map(|p| {
println!("cek {}", p);
p * 2
});
println!("selesai");
}
warning: unused `Map` that must be used
--> src/main.rs:3:5
|
3 | / pesanan.iter().map(|p| {
4 | | println!("cek {}", p);
5 | | p * 2
6 | | });
| |______^
|
= note: iterators are lazy and do nothing unless consumed
Lalu programnya jalan dan cuma mencetak selesai. Itu jawaban tebakannya: gak ada satu pun cek. Adapter itu lazy (malas): map cuma memasang pos di rantai, dan pos itu baru bekerja kalau ada consumer di ujung yang menarik isinya. Compiler pun memperingatkan, iterators are lazy and do nothing unless consumed. Pasang consumer, dan urutan kerjanya kelihatan:
fn main() {
let pesanan = vec![102000, 70000, 38000];
let total: u32 = pesanan
.iter()
.filter(|&&p| {
println!("filter {}", p);
p >= 50000
})
.map(|p| {
println!("map {}", p);
p - p / 10
})
.sum();
println!("Total: {}", total);
}
filter 102000
map 102000
filter 70000
map 70000
filter 38000
Total: 154800
Isinya lewat satu per satu sampai ujung rantai: 102000 disaring lalu dipotong, baru 70000 masuk. 38000 gagal di filter, jadi map-nya gak pernah dipanggil untuknya. Gak ada Vec sementara yang dibuat di tengah. Totalnya 91800 + 63000 = 154800. Rantai seperti ini sama cepatnya dengan loop for yang ditulis manual: abstraksinya gak menambah biaya.
Cara yang sama untuk vec![1, 2, 3, 4] dengan .filter(|&&x| x % 2 == 0).map(|x| x * 10).sum(): yang lolos 2 dan 4, jadi 20 dan 40, totalnya 60.
⚠️ Jebakan yang sering ketemu
- Rantai tanpa consumer. Gak error, cuma warning
unused ... that must be used, dan gak ada yang jalan. Akhiri dengansum,collect,count, ataufor. collect()tanpa tipe. E0283type annotations needed. Tulislet hasil: Vec<u32> = ....- Pinjaman ganda di
filter.|p| p >= 50000ditolak denganexpected `&&_`, found integer. Pakai|&&p|atau**p. - Memakai Vec sesudah
into_iter()ataufor p in pesanan. E0382. Pakaiiter()atau&pesanankalau Vec-nya masih dibutuhkan. - Closure yang mengubah tapi gak
let mut. E0596. ClosureFnMutharus disimpan di variabelmut.
TL;DR: Closure |harga: u32| harga - harga * diskon / 100 adalah fungsi tanpa nama yang menangkap variabel di sekitarnya. Isinya menentukan trait-nya: membaca Fn, mengubah FnMut (closure-nya let mut), menyerahkan miliknya FnOnce (sekali panggil). move membuat closure memiliki tangkapannya, misalnya untuk thread::spawn, tapi closure yang cuma membaca tetap Fn. iter() meminjam &T, iter_mut() meminjam &mut T, into_iter() menghabiskan Vec dan memberi T. Adapter (map, filter, enumerate, zip) itu lazy: gak ada yang jalan sebelum consumer (sum, count, fold, collect dengan tipe, for) menarik isinya, satu per satu sampai ujung rantai.