Pada Chapter sebelumnya kita melihat closure menangkap variabel secara immutable, mutable, dan — sekilas lewat pesan galat — potensi kepemilikan penuh. Sekarang kita perlu menjawab pertanyaan yang selama ini bersembunyi: bagaimana kompilator mengetahui closure mana yang boleh dipanggil sekali, berkali-kali, atau oleh banyak pemanggil sekaligus? Pertanyaan ini bukan akademis. Fungsi yang menerima closure sebagai callback perlu jaminan closure bisa dipanggil beberapa kali; thread yang berbagi data perlu jaminan tidak ada yang mengubah data secara bersamaan. Rust menjawab dengan tiga trait bernama Fn, FnMut, dan FnOnce. Pada Chapter ini kita bedah ketiganya lewat contoh nyata, termasuk galat yang muncul ketika closure jenis sekali-pakai dipaksa bekerja dua kali.

Tiga Trait yang Mengatur Pemanggilan Closure #

Sekarang kita buka mekanisme yang di Chapter sebelumnya hanya disebut sebatas “struct anonim”. Setiap closure literal yang kita tulis memakai kompilator untuk membuat dua hal: struct anonim berisi tangkapan, dan implementasi trait pemanggilan pada struct itu. Trait pemanggilan menentukan bagaimana closure menerima dirinya sendiri saat dipanggil:

  • FnOnce — dipanggil dengan self secara nilai. Pemanggilan mengonsumsi closure; setelah itu closure mati. Semua closure mengimplementasikan FnOnce karena paling sedikit closure pasti bisa dipanggil satu kali.
  • FnMut — dipanggil dengan &mut self. Closure bisa dipanggil berulang karena hanya dipinjam mutable, tetapi setiap pemanggilan boleh mengubah tangkapannya.
  • Fn — dipanggil dengan &self. Closure hanya membaca tangkapan sehingga boleh dipanggil berulang kali, bahkan bersamaan oleh beberapa tempat, tanpa saling mengganggu.

Ketiganya membentuk hierarki: setiap Fn otomatis juga FnMut, dan setiap FnMut otomatis juga FnOnce. Kompilator memilih implementasi “terkuat” yang masuk akal untuk tubuh closure: hanya membaca menghasilkan Fn, mengubah tangkapan menghasilkan FnMut, dan mengeluarkan nilai dari tangkapan menghasilkan FnOnce saja. Karena itu kita tidak pernah menuliskan implementasi trait ini secara manual — kompilator meng-generate semuanya berdasarkan isi tubuh closure.

Fn: Membaca Tanpa Mengubah #

Closure yang tubuhnya hanya membaca tangkapan menjadi Fn murni dan bebas dipanggil berkali-kali:

rust
fn main() {
    let kode_promo = "PROMO-AKHIR-TAHUN";
    let tampilkan_promo = || {
        println!("Gunakan kode {} untuk potongan 20%", kode_promo);
    };

    tampilkan_promo();
    tampilkan_promo();
    tampilkan_promo();
}

Output yang Dihasilkan:

text
Gunakan kode PROMO-AKHIR-TAHUN untuk potongan 20%
Gunakan kode PROMO-AKHIR-TAHUN untuk potongan 20%
Gunakan kode PROMO-AKHIR-TAHUN untuk potongan 20%

Mari kita bedah:

  1. Baris 2 (let kode_promo = "PROMO-AKHIR-TAHUN";): string literal bertipe &str dibuat di scope main.
  2. Baris 3–5 (let tampilkan_promo = || { ... };): closure tanpa parameter memakai kode_promo hanya sebagai bahan cetak — operasi murni baca. Kompilator menangkapnya sebagai reference immutable dan meng-generate implementasi Fn pada struct anonim closure. Notasi parameter kosong || menandakan closure tidak menerima argumen.
  3. Baris 7–9 (tampilkan_promo(); tiga kali): ketiga pemanggilan hanya meminjam closure secara &self. Karena tidak ada perubahan state, hasil ketiganya identik dan closure tetap hidup setelahnya — ciri khas Fn.

FnMut: Mengubah Tangkapan Berulang Kali #

Closure yang mengubah tangkapan turun kelas menjadi FnMut. Closure masih bisa dipanggil berkali-kali — state yang berubah justru terlihat di antar pemanggilan — tetapi setiap pemanggilan membutuhkan mutable borrow atas closure:

rust
fn main() {
    let mut sisa_kuota = 100;
    let mut pakai_kuota = |megabita: u64| {
        sisa_kuota -= megabita;
        println!("Terpakai {} MB, sisa {} MB", megabita, sisa_kuota);
    };

    pakai_kuota(20);
    pakai_kuota(35);
}

Output yang Dihasilkan:

text
Terpakai 20 MB, sisa 80 MB
Terpakai 35 MB, sisa 45 MB

Mari kita bedah:

  1. Baris 3 (let mut pakai_kuota = |megabita: u64| {): dua tanda mut seperti aturan Chapter sebelumnya — variabel tangkapan dan binding closure. Statement sisa_kuota -= megabita memaksa kompilator menangkap sisa_kuota sebagai &mut, sehingga trait yang di-generate adalah FnMut, bukan Fn.
  2. Baris 4 (sisa_kuota -= megabita;): perubahan menembus ke variabel asli di main karena closure memegang mutable reference, bukan salinan.
  3. Baris 8–9 (pakai_kuota(20); dan pakai_kuota(35);): pemanggilan pertama meninggalkan sisa 80 MB, pemanggilan kedua melanjutkan dari 80 MB menjadi 45 MB. State tersimpan di variabel asli dan bertahan antar pemanggilan — FnMut adalah mesin dengan ingatan.

FnOnce: Mengonsumsi Sekali Saja #

Kelas paling berat muncul ketika tubuh closure memindahkan nilai keluar dari tangkapan — misalnya mengirim String ke fungsi yang mengambil kepemilikannya. Nilai yang sudah pindah tidak bisa dipindahkan lagi, sehingga closure hanya bisa dipanggil satu kali:

rust
fn catat_penarikan(nominal: u64, bukti: String) {
    println!("Penarikan Rp{} tercatat dengan bukti {}", nominal, bukti);
}

fn main() {
    let nomor_bukti = String::from("TRX-2026-0442");
    let tarik_dana = || {
        catat_penarikan(500000, nomor_bukti);
    };

    tarik_dana();
}

Output yang Dihasilkan:

text
Penarikan Rp500000 tercatat dengan bukti TRX-2026-0442

Mari kita bedah:

  1. Baris 1 (fn catat_penarikan(nominal: u64, bukti: String)): fungsi ini mengambil kepemilikan parameter bukti — tidak meminjamnya.
  2. Baris 8 (catat_penarikan(500000, nomor_bukti);): di dalam tubuh closure, nomor_bukti dikirim sebagai argumen String by value, sehingga nilainya pindah dari environment closure ke dalam fungsi. Kompilator menandai closure ini FnOnce: setelah satu pemanggilan, nomor_bukti sudah tidak ada lagi di dalam struct anonim.
  3. Baris 11 (tarik_dana();): pemanggilan tunggal sah — dan memang hanya satu yang tersedia.

Membuktikan Batas Satu Pemanggilan #

Mari kita ganti baris terakhir dengan dua pemanggilan dan lihat reaksi kompilator:

rust
fn catat_penarikan(nominal: u64, bukti: String) {
    println!("Penarikan Rp{} tercatat dengan bukti {}", nominal, bukti);
}

fn main() {
    let nomor_bukti = String::from("TRX-2026-0442");
    let tarik_dana = || {
        catat_penarikan(500000, nomor_bukti);
    };

    tarik_dana();
    tarik_dana();
}

Pesan galat asli kompilator:

text
error[E0382]: use of moved value: `tarik_dana`
  --> main.rs:12:5
   |
11 |     tarik_dana();
   |     ------------ `tarik_dana` moved due to this call
12 |     tarik_dana();
   |     ^^^^^^^^^^ value used here after move
   |
note: closure cannot be invoked more than once because it moves the variable `nomor_bukti` out of its environment
  --> main.rs:8:33
   |
 8 |         catat_penarikan(500000, nomor_bukti);
   |                                 ^^^^^^^^^^^
note: this value implements `FnOnce`, which causes it to be moved when called
  --> main.rs:11:5
   |
11 |     tarik_dana();
   |     ^^^^^^^^^^

error: aborting due to 1 previous error

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

Pesan E0382 membuka mesinnya. Pemanggilan pertama tarik_dana() memindahkan closure — memanggil FnOnce mengonsumsi self secara nilai — dan pemanggilan kedua menabrak aturan use of moved value. Bagian note pertama menunjuk akar masalahnya: closure memindahkan nomor_bukti keluar dari environment. E0382 di sini bukan aturan baru, melainkan konsekuensi lanjutan dari move semantics yang sudah kita kenal di Section Ownership dan Memory.

Membaca Hierarki Saat Memilih Bound #

Ketika kita menulis fungsi yang menerima closure — topik Chapter Closure sebagai Parameter — hierarki ini menjadi panduan memilih trait bound: fungsi yang memanggil callback-nya sekali saja boleh menerima FnOnce (kriteria paling longgar sehingga semua closure masuk), fungsi yang memanggil callback berulang kali harus menuntut minimal FnMut, dan fungsi yang menyimpan callback untuk dipanggil sewaktu-waktu oleh banyak pihak paling aman menuntut Fn.

Aturan praktisnya:

Tubuh closure terhadap tangkapanTrait yang di-generateBisa dipanggil
Hanya membacaFn (sekaligus FnMut, FnOnce)Berkali-kali
Membaca dan mengubahFnMut (sekaligus FnOnce)Berkali-kali, satu per satu
Memindahkan nilai keluarFnOnce sajaTepat satu kali

Ringkasan #

Tiga trait mengatur cara closure dipanggil: FnOnce mengonsumsi closure sekali pakai, FnMut meminjamnya mutable untuk pemanggilan berulang yang boleh mengubah tangkapan, dan Fn meminjamnya immutable untuk pemanggilan berulang yang hanya membaca. Kompilator memilih implementasi otomatis berdasarkan perlakuan tubuh closure terhadap tangkapan — membaca, mengubah, atau memindahkan keluar. Closure yang memindahkan nilai keluar gagal dipanggil kedua kalinya dengan E0382, lengkap dengan penjelasan bahwa closure tersebut hanya mengimplementasikan FnOnce. Hierarki ini langsung terpakai pada Chapter berikutnya saat kita memindahkan kepemilikan tangkapan dengan kata kunci move.