Closure menjadi alat yang sesungguhnya ketika dilepas dari variabel dan dikirim ke fungsi lain. Banyak fungsi di Rust adalah higher-order function: pengurutan data menerima aturan pembandingnya, pemrosesan koleksi menerima aturan transformasinya. Fungsi biasa kurang cocok untuk tugas ini karena tidak bisa menangkap environment; closure bisa, dan itulah nilainya. Namun muncul masalah teknis: parameter fungsi wajib diberi tipe, sementara tipe closure adalah tipe anonim yang tidak bisa kita tulis. Pada Chapter ini kita pelajari dua cara Rust mengatasinya — trait bound generik dan impl Fn — plus konsekuensi memilih bound yang keliru.

Masalah: Tipe Closure Tidak Bisa Dituliskan #

Setiap closure literal menghasilkan tipe unik buatan kompilator — struct anonim berisi tangkapan beserta implementasi Fn/FnMut/FnOnce dari Chapter sebelumnya. Nama tipenya tidak pernah diberitahukan dan tidak bisa diketik manual. Lalu bagaimana fungsi mendeklarasikan parameter closure?

Jawabannya lewat trait. Kita tidak menyebut tipe persisnya, melainkan sifat yang dibutuhkan: “parameter ini adalah sesuatu yang bisa dipanggil dengan u64 dan mengembalikan u64”. Sifat itu dirumuskan sebagai trait bound F: Fn(u64) -> u64, persis seperti generic bound <T: Display> dari Section Traits. Kompilator lalu me-monomorphize fungsi untuk setiap tipe closure — mekanisme static dispatch yang sama dengan generik biasa — sehingga pemanggilan closure di dalam fungsi tetap secepat pemanggilan langsung.

Trait Bound Generik #

Bentuk lengkapnya menambahkan parameter generik F pada fungsi — contoh berikut menerima aturan harga apa pun berbentuk Fn(u64) -> u64:

rust
fn terapkan_aturan<F: Fn(u64) -> u64>(hitung: F, harga: u64) -> u64 {
    hitung(harga)
}

fn main() {
    let diskon_member = |harga| harga - 2000;
    let diskon_promo = |harga| harga * 90 / 100;

    println!("Harga member : Rp{}", terapkan_aturan(diskon_member, 50000));
    println!("Harga promo  : Rp{}", terapkan_aturan(diskon_promo, 50000));
}

Output yang Dihasilkan:

text
Harga member : Rp48000
Harga promo  : Rp45000

Mari kita bedah baris demi baris:

  1. Baris 1 (fn terapkan_aturan<F: Fn(u64) -> u64>(hitung: F, harga: u64) -> u64): fungsi punya parameter generik F dengan bound Fn(u64) -> u64, plus parameter biasa harga: u64. Notasi bound-nya dibaca “bisa dipanggil dengan satu argumen u64 dan hasilnya u64” — kurung berisi tipe argumen, panah berisi tipe kembalian.
  2. Baris 2 (hitung(harga)): di dalam fungsi, hitung dipanggil seperti fungsi biasa — bound sudah menjamin pemanggilan semacam ini sah.
  3. Baris 6–7 (let diskon_member = |harga| harga - 2000; dst.): dua closure dibuat tanpa tangkapan sehingga keduanya Fn, namun tetap bertipe anonim berbeda — diskon_member dan diskon_promo tidak setipe walau bentuknya mirip.
  4. Baris 9–10 (terapkan_aturan(diskon_member, 50000) dst.): setiap pemanggilan menginstansiasi F dengan tipe closure yang dikirim — kompilator me-monomorphize fungsi menjadi dua salinan khusus, tanpa dynamic dispatch.

Bentuk Pendek: impl Fn #

Rust menyediakan bentuk lebih ringkas untuk satu parameter closure: kata kunci impl menggantikan deklarasi generik, seperti impl Trait pada argumen di Section Traits:

rust
fn terapkan_aturan(hitung: impl Fn(u64) -> u64, harga: u64) -> u64 {
    hitung(harga)
}

fn main() {
    println!("Harga member : Rp{}", terapkan_aturan(|harga| harga - 2000, 50000));
    println!("Harga promo  : Rp{}", terapkan_aturan(|harga| harga * 90 / 100, 50000));
}

Output yang Dihasilkan:

text
Harga member : Rp48000
Harga promo  : Rp45000

Mari kita bedah perbedaannya:

  1. Baris 1 (fn terapkan_aturan(hitung: impl Fn(u64) -> u64, harga: u64) -> u64): deklarasi <F: ...> hilang; bound menempel langsung pada parameter. Secara semantik keduanya setara — kompilator tetap membuat parameter generik di balik layar dan tetap me-monomorphize per tipe closure.
  2. Baris 5–6 (terapkan_aturan(|harga| harga - 2000, 50000)): closure tidak perlu diikat ke variabel — literal dikirim langsung sebagai argumen, gaya paling umum karena ringkas.
  3. Keterbatasan bentuk impl: saat fungsi menyebut tipe F lebih dari sekali — misalnya dua parameter closure yang harus setipe — bentuk generik <F: ...> lebih tepat; untuk satu parameter lepas, impl Fn lebih bersih.

Bound FnMut untuk Parameter yang Berubah #

Bound Fn menuntut closure hanya membaca tangkapannya. Fungsi yang memanggil callback-nya beberapa kali sambil mengharapkan callback mencatat progres harus memakai bound FnMut, dan konsekuensinya terlihat pada signature: parameter closure perlu ditandai mut di dalam fungsi:

rust
fn proses_cicilan<F: FnMut(u64)>(mut catat: F, total: u64) {
    catat(total / 2);
    catat(total / 2);
}

fn main() {
    let mut nomor_cicilan = 0;
    proses_cicilan(|bagian| {
        nomor_cicilan += 1;
        println!("Cicilan ke-{} sebesar Rp{}", nomor_cicilan, bagian);
    }, 400000);
}

Output yang Dihasilkan:

text
Cicilan ke-1 sebesar Rp200000
Cicilan ke-2 sebesar Rp200000

Mari kita bedah:

  1. Baris 1 (fn proses_cicilan<F: FnMut(u64)>(mut catat: F, total: u64)): bound diganti menjadi FnMut(u64) — closure tanpa nilai kembalian. Tanda mut pada parameter catat bukan hiasan: setiap pemanggilan FnMut meminjam closure secara mutable, dan pinjaman mutable menuntut parameter itu mut.
  2. Baris 2–3 (catat(total / 2); dua kali): fungsi memanggil callback dua kali — keduanya sah karena FnMut mengizinkan pemanggilan berulang, alasan bound Fn tidak cukup di sini.
  3. Baris 7 (let mut nomor_cicilan = 0;): closure menangkap nomor_cicilan secara mutable karena tubuhnya menaikkan nilainya. Closure ini otomatis mengimplementasikan FnMut, cocok dengan bound fungsi.
  4. Baris 8–11: literal closure dikirim langsung; nomor cicilan naik dari 1 ke 2 di antara dua pemanggilan — state tangkapan bertahan karena closure mengubah variabel asli milik main.

Ketika Closure Hanya Bisa FnOnce #

Bound yang terlalu ketat terungkap saat kompilasi. Kode berikut membuat closure yang mengirim String tangkapan ke fungsi simpan_catatan — perpindahan yang menjadikannya FnOnce — lalu mengirimnya ke terapkan_aturan yang menuntut Fn:

rust
fn simpan_catatan(teks: String) {
    println!("Catatan tersimpan: {}", teks);
}

fn terapkan_aturan<F: Fn(u64) -> u64>(hitung: F, harga: u64) -> u64 {
    hitung(harga)
}

fn main() {
    let catatan = String::from("promo musim libur");
    let catat_promo = |harga| {
        simpan_catatan(catatan);
        harga - 2000
    };

    let hasil = terapkan_aturan(catat_promo, 50000);
    println!("Hasil: Rp{}", hasil);
}

Pesan galat asli kompilator:

text
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
  --> main.rs:11:23
   |
11 |     let catat_promo = |harga| {
   |                       ^^^^^^^ this closure implements `FnOnce`, not `Fn`
12 |         simpan_catatan(catatan);
   |                        ------- closure is `FnOnce` because it moves the variable `catatan` out of its environment
...
16 |     let hasil = terapkan_aturan(catat_promo, 50000);
   |                 --------------- ----------- the requirement to implement `Fn` derives from here
   |                 |
   |                 required by a bound introduced by this call
   |
note: required by a bound in `terapkan_aturan`
  --> main.rs:5:23
   |
 5 | fn terapkan_aturan<F: Fn(u64) -> u64>(hitung: F, harga: u64) -> u64 {
   |                       ^^^^^^^^^^^^^^ required by this bound in `terapkan_aturan`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0525`.

Pesan E0525 memuat dua keterangan. Pertama, status closure: catat_promo hanya mengimplementasikan FnOnce karena tubuhnya memindahkan catatan keluar dari environment. Kedua, sumber tuntutan: bound Fn berasal dari pemanggilan di baris 16. Fn menjanjikan pemanggilan berkali-kali — janji yang mustahil dipenuhi closure sekali-pakai, sehingga kompilator menolak sejak awal.

Karena terapkan_aturan hanya memanggil closure satu kali, bound paling jujur adalah FnOnce. Setelah bound diganti, program berjalan:

rust
fn simpan_catatan(teks: String) {
    println!("Catatan tersimpan: {}", teks);
}

fn terapkan_sekali<F: FnOnce(u64) -> u64>(hitung: F, harga: u64) -> u64 {
    hitung(harga)
}

fn main() {
    let catatan = String::from("promo musim libur");
    let catat_promo = |harga| {
        simpan_catatan(catatan);
        harga - 2000
    };

    let hasil = terapkan_sekali(catat_promo, 50000);
    println!("Hasil: Rp{}", hasil);
}

Output yang Dihasilkan:

text
Catatan tersimpan: promo musim libur
Hasil: Rp48000

Ringkasan #

Fungsi menerima closure lewat trait bound karena tipe closure anonim: bentuk generik <F: Fn(u64) -> u64> paling fleksibel, bentuk impl Fn(u64) -> u64 paling ringkas untuk satu parameter, dan keduanya di-monomorphize menjadi kode spesifik per tipe closure. Bound FnMut dibutuhkan bila fungsi memanggil closure berulang kali dan closure mengubah tangkapannya. Bound yang terlalu ketat ditolak dengan E0525 yang menjelaskan closure hanya FnOnce sementara tuntutan Fn berasal dari bound fungsi. Arah sebaliknya — mengembalikan closure dari fungsi — menyimpan tantangan tipenya sendiri, dan itu pokok bahasan Chapter berikutnya.