Closure

Fn, FnMut, FnOnce, dan penangkapan lingkungan sekitar.

Daftar isi

Topik ini membahas closure, yaitu fungsi tanpa nama yang dapat memakai variabel dari lingkungan sekitar tempat closure itu dibuat. Pembahasan mencakup sintaks closure beserta perbedaannya dari fungsi biasa, tiga mode penangkapan lingkungan beserta kata kunci move, dan tiga trait yang mengatur pemanggilannya, yaitu Fn, FnMut, dan FnOnce. Setelah membaca topik ini, closure dapat ditulis untuk kebutuhan metode pustaka standar yang menerima argumen fungsi, dan pesan error yang berkenaan dengan penangkapan lingkungan dapat dibaca sampai ke penyebabnya.

Closure sebagai fungsi tanpa nama#

Closure adalah fungsi tanpa nama yang disimpan pada variabel, dikirim sebagai argumen, atau dikembalikan dari fungsi lain. Parameternya ditulis di antara dua garis tegak, misalnya |x|, sedangkan isinya dapat berupa satu ekspresi tanpa kurung kurawal atau blok lengkap dengan kurung kurawal. Perbedaan terpenting dari fungsi yang dideklarasikan dengan fn ada dua. Pertama, tipe parameter dan tipe hasil closure umumnya disimpulkan kompilator dari pemakaiannya, sehingga anotasi menjadi opsional. Kedua, closure dapat menangkap variabel dari lingkungan sekitarnya, sesuatu yang tidak dapat dilakukan fungsi biasa.

rust dasar.rs
fn main() {
    let tambah_satu = |x: i32| -> i32 { x + 1 };
    let kali = |x: i32, y: i32| x * y;

    println!("tambah_satu(41): {}", tambah_satu(41));
    println!("kali(6, 7)     : {}", kali(6, 7));

    let faktor = 10;
    let skala = |x: i32| x * faktor;
    println!("skala(4)       : {}", skala(4));
}
text Keluaran
tambah_satu(41): 42
kali(6, 7)     : 42
skala(4)       : 40

Dua closure pertama bekerja seperti fungsi biasa yang diberi nama variabel, dan pemanggilannya menghasilkan 42 dari dua perhitungan yang berbeda. Closure skala memperlihatkan kemampuan khas closure, yaitu memakai variabel faktor dari luar tubuhnya tanpa faktor pernah dikirim sebagai argumen. Ketika skala dibuat, kompilator menyisipkan akses ke faktor ke dalam tubuh closure, sehingga skala(4) benar-benar menghitung 4 * 10. Setiap closure memiliki tipe anonim yang unik, sehingga dua closure dengan isi yang sama tetap merupakan dua tipe yang berbeda dan tidak dapat saling menggantikan pada variabel yang sama.

Konsekuensi tipe anonim tersebut terasa saat closure disimpan pada tipe fungsi biasa. Tipe fn(i32) -> i32, yang disebut function pointer, hanya mampu menampung fungsi dan closure yang tidak menangkap apa pun, karena function pointer sekadar alamat kode tanpa tempat penyimpanan lingkungan.

rust Fragmen: closure penangkap tidak muat dalam function pointer
// tidak-dijalankan
let faktor = 2;
let penangkap = |x: i32| x * faktor;  // menangkap faktor dari lingkungan
let alamat: fn(i32) -> i32 = penangkap; // error: tipe closure tidak cocok dengan fn pointer

Fragmen tersebut ditolak karena penangkap membawa data tangkapan, sedangkan fn(i32) -> i32 tidak memiliki tempat untuk data itu. Agar closure penangkap dapat diterima fungsi, parameter fungsi ditulis sebagai tipe generik dengan batasan trait Fn, FnMut, atau FnOnce, sebagaimana dibahas pada bagian berikutnya.

Penangkapan lingkungan sekitar#

Ketika tubuh closure memakai variabel luar, kompilator menangkap variabel itu dengan cara paling ringan yang masih memadai untuk tubuhnya. Variabel yang hanya dibaca ditangkap lewat referensi tetap, variabel yang diubah ditangkap lewat referensi mutable, dan variabel yang dipindahkan keluar, misalnya dijadikan nilai kembalian String, ditangkap berdasarkan nilainya. Pemilihan mode ini berlangsung otomatis, dan dua closure yang memakai variabel yang sama dapat menangkapnya dengan cara berbeda karena tubuh keduanya berbeda.

rust penangkapan.rs
fn main() {
    // penangkapan lewat referensi tetap: closure hanya membaca
    let daftar = vec![10, 20, 30];
    let jumlah = || daftar.len();
    println!("panjang daftar: {}", jumlah());
    println!("daftar tetap   : {:?}", daftar);

    // penangkapan lewat referensi mutable: closure mengubah lingkungan
    let mut hitung = 0;
    let mut naik = || {
        hitung += 1;
        hitung
    };
    println!("naik pertama: {}", naik());
    println!("naik kedua  : {}", naik());

    // penangkapan dengan move: kepemilikan berpindah ke closure
    let pemilik = String::from("catatan sensor");
    let pindah = move || println!("milik closure: {}", pemilik);
    pindah();
}
text Keluaran
panjang daftar: 3
daftar tetap   : [10, 20, 30]
naik pertama: 1
naik kedua  : 2
milik closure: catatan sensor

Closure jumlah hanya membaca daftar, sehingga daftar ditangkap lewat referensi tetap dan masih dapat dicetak setelahnya. Closure naik menambah hitung, sehingga penangkapannya memakai referensi mutable, dan karena itu variabel closure naik sendiri juga wajib mut. Kata kunci move pada closure pindah memaksa penangkapan berdasarkan nilai tanpa melihat isi tubuhnya, sehingga kepemilikan pemilik berpindah ke dalam closure dan variabel asalnya tidak dapat dipakai lagi. Penangkapan move diperlukan ketika closure akan hidup lebih lama daripada lingkup tempat ia dibuat, misalnya saat dikirim ke thread lain.

Tiga trait pemanggilan: Fn, FnMut, dan FnOnce#

Sebuah closure adalah nilai dari tipe anonim yang menyimpan seluruh tangkapannya sebagai medan. Nilai itu dapat dipanggil karena tipe anonimnya mengimplementasikan salah satu dari tiga trait di std::ops, dan ketiganya membentuk hierarki. Trait FnOnce menerima closure berdasarkan nilainya sehingga closure hanya dapat dipanggil sekali. Trait FnMut meminjam closure secara mutable sehingga lingkungan dapat berubah pada setiap pemanggilan. Trait Fn meminjam closure secara tetap sehingga lingkungan hanya dibaca. Setiap closure Fn sekaligus FnMut dan FnOnce, dan setiap closure FnMut sekaligus FnOnce.

TraitPenerima pada selfAkses lingkunganCocok untuk
Fn&selfmembaca tanpa mengubahperhitungan yang dapat diulang bebas
FnMut&mut selfmembaca dan mengubahpencacah atau penampung sementara
FnOnceselfmenghabiskan tangkapanpemanggilan tunggal yang memindahkan nilai

Ketiga trait itu dipakai sebagai batasan ketika fungsi menerima closure. Program berikut mendefinisikan dua fungsi semacam itu, satu dengan batasan Fn karena dipanggil dua kali, dan satu dengan batasan FnOnce karena hasilnya memindahkan String.

rust tiga-trait.rs
fn panggil_dua_kali<F: Fn(i32) -> i32>(f: F, nilai: i32) {
    println!("panggilan pertama: {}", f(nilai));
    println!("panggilan kedua  : {}", f(nilai));
}

fn panggil_sekali<F: FnOnce() -> String>(f: F) {
    println!("hasil tunggal: {}", f());
}

fn main() {
    let faktor = 5;
    let ubah = |x| x * faktor;
    panggil_dua_kali(ubah, 8);

    let pesan = String::from("laporan harian");
    let habiskan = move || format!("{} selesai", pesan);
    panggil_sekali(habiskan);

    let mut total = 0;
    {
        let mut akumulator = |n: i32| {
            total += n;
            total
        };
        println!("akumulator(10): {}", akumulator(10));
        println!("akumulator(5) : {}", akumulator(5));
    }
    println!("total akhir   : {}", total);
}
text Keluaran
panggilan pertama: 40
panggilan kedua  : 40
hasil tunggal: laporan harian selesai
akumulator(10): 10
akumulator(5) : 15
total akhir   : 15

Closure ubah hanya membaca faktor, sehingga closure itu mengimplementasikan Fn dan lolos batasan panggil_dua_kali yang memanggil argumennya dua kali dengan hasil yang sama. Closure habiskan memindahkan pesan ke dalam String yang dibentuk format!, sehingga closure itu hanya mengimplementasikan FnOnce dan hanya cocok pada panggil_sekali. Closure akumulator mengubah total pada setiap pemanggilan, dan nilai yang dicetak naik dari 10 ke 15 karena lingkungan yang ditangkapnya berubah di antara kedua pemanggilan. Pada perancangan API, batasan paling ringan yang masih memadai memberi ruang pemanggilan terluas, karena batasan Fn menerima ketiga jenis closure, sedangkan batasan FnOnce membuat fungsi tidak dapat memanggil argumennya dua kali.

Kesalahan umum#

Lupa mut pada variabel closure yang mengubah lingkungan. Closure FnMut mengubah tangkapannya, sehingga variabel penampungnya wajib dideklarasikan mut, dan kelalaian ini ditolak dengan error E0596 yang menyebut variabel tersebut. Tambahkan mut pada variabel closure, bukan pada variabel yang ditangkap, apabila penangkapan memang berlangsung lewat referensi.

Menganggap move selalu melumpuhkan variabel asal. Pada tipe Copy, penangkapan move menyalin nilai sehingga variabel asal tetap hidup dan dapat dicetak. Kehilangan akses hanya terjadi pada tipe pemilik memori, sehingga pemeriksaan seperti itu perlu dilakukan per tipe, bukan sekadar melihat ada tidaknya move.

Menyimpan closure penangkap pada function pointer. Tipe fn(...) -> ... hanya menampung fungsi dan closure tanpa tangkapan, sehingga penetapan closure penangkap selalu ditolak sebagai ketidakcocokan tipe. Gunakan parameter generik dengan batasan Fn atau FnMut, atau bungkus closure dalam Box<dyn Fn> ketika tipe konkret tidak boleh muncul pada tanda tangan.

Ringkasan#

  • Closure adalah fungsi tanpa nama dengan tipe anonim yang menangkap lingkungan: lewat referensi tetap untuk membaca, lewat referensi mutable untuk mengubah, dan berdasarkan nilai dengan move untuk kepemilikan penuh.
  • Fn memanggil tanpa mengubah, FnMut mengubah pada tiap pemanggilan, dan FnOnce menghabiskan closure tepat satu kali, dengan hierarki Fn sekaligus FnMut sekaligus FnOnce.
  • Batasan trait pada parameter fungsi menentukan jenis closure yang diterima, dan batasan paling ringan yang memadai memberi ruang pemanggilan terluas.
  • Function pointer fn(...) hanya menerima closure tanpa tangkapan, sehingga closure penangkap memerlukan batasan trait atau Box<dyn Fn>.

Referensi#