Fungsi biasa hanya bisa bekerja dengan tiga sumber data: parameter yang dikirim pemanggil, nilai yang dibuat di dalam tubuhnya, dan konstanta global. Ketika kita menulis potongan logika kecil di tengah main, data yang dibutuhkan logika itu sering kali sudah tersedia di sekitarnya — persentase diskon yang dihitung dari konfigurasi, sisa stok yang berubah sepanjang program, atau daftar nama pelanggan. Closure memiliki kemampuan yang tidak dimiliki fungsi biasa: closure bisa memakai variabel-variabel tersebut secara langsung. Kemampuan ini disebut menangkap environment, dan inilah alasan utama closure tidak sekadar “fungsi ringkas”. Pada Chapter ini kita akan melihat dua mode penangkapan — immutable dan mutable — beserta aturan mut yang menyertainya.

Apa itu Environment Closure #

Environment adalah kumpulan variabel yang berada dalam scope pada saat closure dibuat, bukan saat closure dipanggil. Ketika tubuh closure menyebut salah satu variabel tersebut, kompilator mencatatnya sebagai tangkapan (capture) dan menyertakannya sebagai bagian dari nilai closure. Mekanismenya tidak mistis: kompilator meng-generate sebuah struct anonim yang menyimpan setiap variabel yang ditangkap — berupa reference untuk penangkapan pinjaman, atau nilai utuh untuk penangkapan kepemilikan — lalu menjadikan struct itu field tersembunyi di dalam closure. Detail trait dari struct anonim ini kita bedah tuntas di Chapter Fn, FnMut, FnOnce.

Sketsa berikut memperlihatkan posisi struct anonim tersebut:

Diagram di atas menegaskan satu hal penting: closure dan variabel yang ditangkap tetap dua entitas terpisah. Variabel tetap dimiliki scope asalnya, sementara closure hanya membawa akses menuju variabel tersebut — sama seperti hubungan pemilik dan peminjam yang sudah kita kenal di Section Ownership dan Memory.

Menangkap Variabel secara Immutable #

Mode penangkapan paling ringan terjadi ketika tubuh closure hanya membaca variabel. Kompilator menangkap variabel sebagai reference immutable, sehingga closure sama sekali tidak mengganggu pemiliknya:

rust
fn main() {
    let persen_diskon: u64 = 10;
    let hitung_diskon = |harga: u64| harga * persen_diskon / 100;

    println!("Diskon dari Rp{} : Rp{}", 50000, hitung_diskon(50000));
    println!("Diskon dari Rp{} : Rp{}", 80000, hitung_diskon(80000));
    println!("Persen tetap     : {}%", persen_diskon);
}

Output yang Dihasilkan:

text
Diskon dari Rp50000 : Rp5000
Diskon dari Rp80000 : Rp8000
Persen tetap     : 10%

Mari kita bedah baris-baris pentingnya:

  1. Baris 2 (let persen_diskon: u64 = 10;): variabel persen_diskon dibuat di scope main dan dimiliki oleh main. Variabel ini bukan parameter closure dan bukan juga variabel lokal closure.
  2. Baris 3 (let hitung_diskon = |harga: u64| harga * persen_diskon / 100;): tubuh closure menyebut persen_diskon, sehingga kompilator menangkap variabel itu ke dalam struct anonim closure — dalam bentuk reference immutable &u64. Parameter harga tetap datang dari pemanggilan, bukan dari environment.
  3. Baris 5–6 (hitung_diskon(50000) dan hitung_diskon(80000)): closure dipanggil dua kali dan dua kali pula membaca persen_diskon lewat reference yang disimpannya. Penangkapan sekali dipakai berkali-kali — tidak ada penyalinan nilai antar pemanggilan.
  4. Baris 7 (println!("Persen tetap : {}%", persen_diskon);): persen_diskon masih sah dipakai main secara penuh. Penangkapan immutable tidak mengambil kepemilikan dan tidak mengunci variabel dari pemiliknya.

Contoh ini memperlihatkan kenapa closure sering menggantikan “parameter tambahan”: alih-alih mengirim persen_diskon sebagai argumen ke setiap pemanggilan, closure membawa aksesnya sendiri.

Menangkap Variabel secara Mutable #

Cerita berbeda ketika tubuh closure mengubah variabel yang ditangkap. Kompilator lalu menangkap variabel sebagai reference mutable &mut, dan konsekuensinya menjalar ke dua tempat sekaligus: variabel yang ditangkap harus dideklarasikan mut, dan binding closure itu sendiri juga harus mut:

rust
fn main() {
    let mut poin_pelanggan = 0;
    let mut catat_poin = |poin: u64| {
        poin_pelanggan += poin;
    };

    catat_poin(10);
    catat_poin(25);
    catat_poin(40);
    println!("Poin akhir pelanggan: {}", poin_pelanggan);
}

Output yang Dihasilkan:

text
Poin akhir pelanggan: 75

Mari kita bedah aturan yang bekerja di balik kode ini:

  1. Baris 2 (let mut poin_pelanggan = 0;): tanda mut pertama wajib karena poin_pelanggan akan diubah dari dalam closure. Tanpa mut ini, statement poin_pelanggan += poin di dalam tubuh closure langsung ditolak.
  2. Baris 3 (let mut catat_poin = |poin: u64| {): tanda mut kedua wajib pada binding closure. Alasannya: setiap kali closure mutable dipanggil, closure meminjamkan kembali &mut poin_pelanggan ke tubuhnya, dan meminjamkan sesuatu yang dimiliki closure berarti meminjamkan closure itu sendiri secara mutable. Binding yang tidak mut tidak boleh dipinjam mutable.
  3. Baris 7–9 (catat_poin(10); dst.): tiga pemanggilan menambah nilai poin_pelanggan secara kumulatif. Nilai tersimpan di variabel asli di scope main — closure tidak memiliki salinan sendiri, sehingga perubahan langsung terlihat dari luar.
  4. Baris 10 (println!("Poin akhir pelanggan: {}", poin_pelanggan);): main membaca poin_pelanggan setelah pemanggilan terakhir. Pinjaman mutable yang dipegang closure berakhir pada pemanggilan terakhirnya (aturan non-lexical lifetime dari Section Lifetime), sehingga pembacaan setelahnya sah.

Kode yang Gagal: Closure Tanpa mut #

Mari kita buktikan kelalaian yang disebut callout di atas. Kode berikut mendeklarasikan closure yang mengurangi stok, tetapi lupa menandai binding closure sebagai mut:

rust
fn main() {
    let mut stok_kopi = 7;
    let kurangi_stok = || {
        stok_kopi -= 1;
    };

    kurangi_stok();
    println!("Stok tersisa: {}", stok_kopi);
}

Kompilasi gagal dengan pesan asli berikut:

text
error[E0596]: cannot borrow `kurangi_stok` as mutable, as it is not declared as mutable
 --> main.rs:7:5
  |
4 |         stok_kopi -= 1;
  |         --------- calling `kurangi_stok` requires mutable binding due to mutable borrow of `stok_kopi`
...
7 |     kurangi_stok();
  |     ^^^^^^^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
3 |     let mut kurangi_stok = || {
  |         +++

error: aborting due to 1 previous error

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

Pesan E0596 patut dibaca pelan-pelan karena isinya dua lapis. Lapis pertama menjelaskan mengapa closure membutuhkan mutable binding: statement stok_kopi -= 1 menuntut mutable borrow atas stok_kopi, dan tuntutan itu menjadikan seluruh closure bermuatan mutable. Lapis kedua menunjuk di mana aturan dilanggar: pemanggilan kurangi_stok() di baris 7 meminjam closure secara mutable, padahal binding-nya tidak dideklarasikan mut. Kompilator bahkan menuliskan perbaikan lengkapnya — ubah baris deklarasi menjadi let mut kurangi_stok = || { — sehingga memperbaiki galat ini hanya soal menambah tiga karakter.

Mode Penangkapan Dipilih Kompilator #

Kita tidak pernah menuliskan “tangkap persen_diskon secara immutable” di kode. Kompilator memilih mode penangkapan setiap variabel secara otomatis dengan prinsip seringan mungkin: hanya membaca berarti reference immutable, perlu mengubah berarti reference mutable, dan hanya ketika tubuh closure memaksa kepemilikan — misalnya memindahkan nilai keluar dari environment — kompilator menangkap dengan memindahkan kepemilikan. Prinsip “seringan mungkin” inilah yang membuat closure hemat tanpa kita mengaturnya.

Satu mode penangkapan belum kita sentuh: memindahkan kepemilikan penuh ke dalam closure atas kemauan kita sendiri, bukan karena kompilator memutuskan. Mode itu dinyalakan kata kunci move dan menjadi pokok bahasan Chapter Closure dengan move.

Ringkasan #

Closure dapat memakai variabel dari scope tempat closure dibuat — kemampuan menangkap environment. Penangkapan immutable menyimpan reference biasa di struct anonim closure sehingga variabel asli tetap hidup dan bebas dipakai pemiliknya. Penangkapan mutable menuntut dua tanda mut: satu pada variabel yang ditangkap, satu pada binding closure; kelalaian pada binding memunculkan E0596 lengkap dengan saran perbaikan. Kompilator memilih mode penangkapan otomatis dengan prinsip seringan mungkin, dan mode terberat — kepemilikan penuh — dapat kita paksa dengan move.