Sepanjang Course Fungsi kita selalu menulis logika dengan pola yang sama: kata kunci fn, nama fungsi, daftar parameter lengkap dengan tipe masing-masing, lalu tubuh fungsi di dalam kurung kurawal. Ritual tersebut memang pantas untuk fungsi yang dipanggil dari banyak tempat, tetapi ritual yang sama terasa berat ketika kita hanya butuh potongan hitung-hitungan sekali pakai — misalnya aturan diskon khusus yang hanya berlaku di satu cabang toko. Rust menyediakan closure untuk kebutuhan semacam itu: fungsi anonim tanpa nama yang ditulis langsung di tempat nilai dibutuhkan, dengan bentuk yang jauh lebih ringkas daripada fn. Pada Chapter ini kita akan membedah bentuk dasar closure, membiasakan diri dengan pipe syntax, dan melihat bagaimana inferensi tipe membuat sebagian besar anotasi tipe menjadi tidak perlu.
Fungsi dan Closure dengan Tugas yang Sama #
Kita mulai dengan membandingkan dua potongan kode yang menghitung hal identik: satu memakai fungsi biasa, satu memakai closure. Keduanya memotong harga tetap sebesar Rp2.000.
fn diskon_tetap(harga: u64) -> u64 {
harga - 2000
}
fn main() {
let diskon_closure = |harga: u64| harga - 2000;
println!("Fungsi : Rp{}", diskon_tetap(25000));
println!("Closure : Rp{}", diskon_closure(25000));
}Output yang Dihasilkan:
Fungsi : Rp23000
Closure : Rp23000Kedua pemanggilan menghasilkan angka yang sama persis. Mari kita bedah perbedaan bentuknya:
- Baris 1 (
fn diskon_tetap(harga: u64) -> u64 {): fungsi biasa wajib punya namadiskon_tetap, wajib menyatakan tipe parameterharga: u64, dan wajib menyatakan tipe kembalian-> u64. Ketiga kewajiban ini muncul karena fungsi adalah antarmuka yang dipanggil dari lokasi mana saja, sehingga kontraknya harus eksplisit. - Baris 6 (
let diskon_closure = |harga: u64| harga - 2000;): closure dibuat sebagai ekspresi lalu diikat ke variabeldiskon_closuredenganlet, sama seperti nilai biasa. Bagian|harga: u64|adalah daftar parameter diapit dua tanda pipe, danharga - 2000adalah tubuh closure — tidak ada kata kuncifn, tidak ada nama, dan tidak ada-> u64. - Baris 6 (tubuh
harga - 2000): tubuh berupa satu ekspresi tanpa titik koma, sehingga hasil ekspresi menjadi nilai kembalian closure. Perilaku ini sama dengan aturan ekspresi terakhir yang sudah kita kenal di fungsi. - Baris 8–9 (
diskon_tetap(25000)dandiskon_closure(25000)): keduanya dipanggil dengan tanda kurung dan argumen25000. Dari sisi pemanggil, closure dan fungsi tidak terasa berbeda.
Poin pentingnya ada di baris 6: closure adalah nilai. Variabel diskon_closure bisa dipindahkan ke variabel lain, dikirim ke fungsi, dikembalikan dari fungsi, atau disimpan dalam struct — hal-hal yang tidak bisa dilakukan pada fungsi biasa tanpa bantuan function pointer. Kedalaman topik ini akan kita buka bertahap sepanjang Section Closure.
Anatomi Pipe Syntax #
Bentuk umum closure adalah |parameter| tubuh. Tanda pipe menggantikan tanda kurung daftar parameter, dan tubuh ditulis langsung setelahnya. Tubuh boleh berupa satu ekspresi seperti contoh pertama, atau blok { } berisi banyak statement — kita bahas blok di bagian akhir Chapter.
Keunggulan terbesar closure dibanding fungsi adalah inferensi tipe. Fungsi harus menyatakan tipe parameter secara penuh; closure boleh mengabaikannya dan membiarkan kompilator menyimpulkan dari cara closure dipakai:
fn main() {
let tambah_pajak = |harga| harga + harga / 10;
println!("Total belanja: Rp{}", tambah_pajak(50000));
}Output yang Dihasilkan:
Total belanja: Rp55000Mari kita bedah cara kompilator menentukan tipe:
- Baris 2 (
let tambah_pajak = |harga| harga + harga / 10;): parameterhargaditulis tanpa anotasi tipe. Pada saat baris ini dikompilasi, tipehargamasih cair — kompilator menunda keputusan sampai melihat pemanggilan pertama. - Baris 2 (operasi
harga + harga / 10): operasi aritmetika dan literal10memberi petunjuk bahwahargaharus bertipe integer, tetapi integer yang mana (i32,u64, lainnya) masih menunggu konteks pemanggilan. - Baris 4 (
tambah_pajak(50000)): argumen literal50000bertipei32(default integer literal di Rust). Kompilator akhirnya membekukan parameterhargamenjadii32, dan seluruh closure menjadi|harga: i32| -> i32.
Alasan inferensi berbeda antara fungsi dan closure sederhana: closure biasanya berumur pendek dan dipakai di tempat yang sempit, sehingga kompilator punya konteks pemanggilan yang jelas. Fungsi adalah antarmuka global yang bisa dipanggil dari mana saja, jadi kontrak tipenya harus ditulis penuh.
Inferensi Hanya Sekali Putus #
Kekuatan inferensi tipe punya batas yang sering menjegal pemula: tipe parameter closure dibekukan pada pemanggilan pertama, dan keputusan itu tidak bisa diubah. Kode berikut mencoba memakai closure yang sama untuk angka dan teks:
fn main() {
let tambah_pajak = |harga| harga + harga / 10;
println!("Total belanja: Rp{}", tambah_pajak(50000));
println!("Nama cabang : {}", tambah_pajak("Kopi Nusantara"));
}Kode di atas gagal dikompilasi. Pesan error asli dari kompilator:
error[E0308]: mismatched types
--> main.rs:5:47
|
5 | println!("Nama cabang : {}", tambah_pajak("Kopi Nusantara"));
| ------------ ^^^^^^^^^^^^^^^^ expected integer, found `&str`
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `{integer}`
--> main.rs:4:50
|
4 | println!("Total belanja: Rp{}", tambah_pajak(50000));
| ------------ ^^^^^ expected because this argument is of type `{integer}`
| |
| in this closure call
note: closure parameter defined here
--> main.rs:2:25
|
2 | let tambah_pajak = |harga| harga + harga / 10;
| ^^^^^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.Pesan E0308 menjelaskan kejadian dengan tepat. Pemanggilan pertama dengan 50000 membuat kompilator menyimpulkan harga bertipe integer, dan kesimpulan itu dikunci permanen. Pemanggilan kedua dengan "Kopi Nusantara" bertipe &str langsung ditolak dengan mismatched types. Solusinya ada dua: buat closure kedua untuk teks, atau tulis anotasi tipe secara eksplisit pada parameter agar niat kita jelas sejak awal — misalnya |harga: u64| — dan pastikan semua pemanggilan mengirim tipe yang cocok.
Tubuh Berupa Ekspresi dan Tubuh Berupa Blok #
Seluruh contoh sejauh ini memakai tubuh satu ekspresi. Ketika logika butuh beberapa statement, tubuh closure ditulis sebagai blok { } dengan aturan yang sama seperti tubuh fungsi: statement ditandai titik koma, dan ekspresi terakhir tanpa titik koma menjadi nilai kembalian. Closure berikut memakai dua parameter dan blok multi-baris untuk mencetak struk pembelian:
fn main() {
let tampilkan_struk = |nama: &str, total: u64| {
let pajak = total / 10;
println!("Pelanggan : {}", nama);
println!("Total : Rp{}", total);
println!("Pajak : Rp{}", pajak);
println!("Bayar : Rp{}", total + pajak);
};
tampilkan_struk("Siti Rahma", 120000);
}Output yang Dihasilkan:
Pelanggan : Siti Rahma
Total : Rp120000
Pajak : Rp12000
Bayar : Rp132000Mari kita bedah potongan di atas:
- Baris 2 (
let tampilkan_struk = |nama: &str, total: u64| {): closure dua parameter dipisah koma di antara pipe. Parameternama: &strmemakai reference — tipe yang sudah kita kenal di Section Ownership dan Memory — dan di sini anotasi tipe ditulis eksplisit karena closure tidak mengembalikan nilai sehingga konteks inferensinya tipis. - Baris 3 (
let pajak = total / 10;): statement biasa di dalam blok. Pembagian integer memotong sisa, sama seperti perilakuu64yang kita bahas di dasar sintaks. - Baris 4–7 (
println!(...)): empat statement cetak dijalankan berurutan setiap kali closure dipanggil. Baris terakhir berakhir titik koma, sehingga closure ini tidak mengembalikan nilai — nilai kembaliannya adalah()seperti fungsi tanpareturn. - Baris 10 (
tampilkan_struk("Siti Rahma", 120000);): pemanggilan mengirim dua argumen sesuai urutan parameter.
Ringkasan #
Closure adalah fungsi anonim berbentuk |parameter| tubuh yang diikat ke variabel dengan let dan dipanggil dengan tanda kurung seperti fungsi biasa. Parameter boleh ditulis tanpa tipe karena kompilator menyimpulkan dari pemanggilan pertama, tetapi kesimpulan itu permanen — pemanggilan dengan tipe berbeda memicu E0308. Tubuh closure boleh satu ekspresi atau blok { } multi-statement dengan aturan ekspresi terakhir yang sama seperti fungsi. Dengan bentuk dasar ini di tangan, kita siap membahas fitur yang benar-benar membedakan closure dari fungsi: kemampuan mengambil data dari lingkungan sekitarnya.