Closures & Iterators — Rust

Closures & Iterators di Rust Closure adalah fungsi anonim yang bisa menangkap variabel dari lingkungan sekitarnya. Digabung dengan iterator, kamu bisa menulis…

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 }); lalu println!("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%

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

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

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

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

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.

Yang akan kamu pelajari