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.
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));
}tambah_satu(41): 42
kali(6, 7) : 42
skala(4) : 40Dua 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.
// 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.
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();
}panjang daftar: 3
daftar tetap : [10, 20, 30]
naik pertama: 1
naik kedua : 2
milik closure: catatan sensorClosure 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.
| Trait | Penerima pada self | Akses lingkungan | Cocok untuk |
|---|---|---|---|
Fn | &self | membaca tanpa mengubah | perhitungan yang dapat diulang bebas |
FnMut | &mut self | membaca dan mengubah | pencacah atau penampung sementara |
FnOnce | self | menghabiskan tangkapan | pemanggilan 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.
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);
}panggilan pertama: 40
panggilan kedua : 40
hasil tunggal: laporan harian selesai
akumulator(10): 10
akumulator(5) : 15
total akhir : 15Closure 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
moveuntuk kepemilikan penuh. Fnmemanggil tanpa mengubah,FnMutmengubah pada tiap pemanggilan, danFnOncemenghabiskan closure tepat satu kali, dengan hierarkiFnsekaligusFnMutsekaligusFnOnce.- 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 atauBox<dyn Fn>.
Referensi#
- Closures: Anonymous Functions that Capture Their Environment — The Rust Programming Language
— Bab resmi tentang sintaks closure, penangkapan dengan
move, dan ketiga trait pemanggilan. - Module std::ops — Rust standard library
— Definisi trait
Fn,FnMut, danFnOncebeserta hierarkinya. - Closures — Rust By Example — Kumpulan contoh ringkas penangkapan dan pemanggilan closure.