Lewati ke konten

Fn, FnMut, dan FnOnce di Rust

Di halaman Closure, kamu sudah belajar cara menulis closure (fungsi anonim), cara ia menangkap variabel, dan cara ia mengubahnya. Halaman ini menjawab pertanyaan berikutnya: bagaimana cara menulis fungsi buatanmu sendiri yang menerima closure? Dan bagaimana cara mengembalikan closure dari sebuah fungsi?

Jawabannya ada di tiga trait kecil: Fn, FnMut, dan FnOnce. Bayangkan aturan di perpustakaan. Ada pengunjung yang cuma membaca buku (Fn). Ada yang menulis catatan di pinggir halaman (FnMut). Dan ada yang membawa pulang bukunya dan tidak pernah mengembalikannya (FnOnce). Rust melihat apa yang dilakukan closure-mu, lalu memberinya label yang cocok. Kalau trait-trait ini kelihatan menakutkan di awal, tenang saja. Jumlahnya cuma tiga, dan compiler yang memilihkan yang tepat untukmu.

Setiap closure punya tipe uniknya sendiri yang dibuat oleh compiler. Kamu tidak bisa menuliskan nama tipe itu. Bahkan dua closure yang kelihatan persis sama pun punya tipe yang berbeda:

fn main() {
let mut rules = vec![|price: u32| price * 2];
rules.push(|price: u32| price * 2);
}
error[E0308]: mismatched types
--> src/main.rs:3:16
|
3 | rules.push(|price: u32| price * 2);
| ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object

Compiler bilang tipenya tidak cocok: ia mengharapkan closure yang pertama, tapi menemukan closure lain, karena tidak ada dua closure yang punya tipe sama. Jadi sebuah fungsi tidak bisa bilang “beri aku closure bertipe X”. Sebagai gantinya, fungsi itu bilang “beri aku tipe apa saja yang bisa dipanggil seperti ini”. Itulah trait bound, sama seperti yang ada di halaman Generics dan Trait:

fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 {
rule(price)
}
fn main() {
let coupon = 15;
let half_price = |price: u32| price / 2;
let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80));
println!("{}", price_after(use_coupon, 80));
}

Output:

40
65
▶ Coba sendiri

Baca F: Fn(u32) -> u32 sebagai: “F adalah tipe yang bisa dipanggil dengan sebuah u32, dan mengembalikan u32.” Kedua closure cocok, walaupun tipenya berbeda.

Compiler melihat apa yang dilakukan isi closure terhadap nilai-nilai yang ditangkapnya:

  • Fn: closure hanya membaca nilai itu. Kamu bisa memanggilnya sesering yang kamu mau.
  • FnMut: closure mengubah nilai itu. Kamu bisa memanggilnya berkali-kali, tapi butuh mut.
  • FnOnce: closure menyerahkan nilai itu (memindahkannya keluar). Kamu hanya bisa memanggilnya sekali, karena setelah itu nilainya sudah tidak ada.
fn main() {
let shop = String::from("Toko Roti Pojok");
// Fn: hanya membaca `shop`
let greet = || println!("Selamat datang di {shop}!");
greet();
greet();
// FnMut: mengubah `orders`
let mut orders = 0;
let mut take_order = || {
orders += 1;
println!("pesanan #{orders} dicatat");
};
take_order();
take_order();
// FnOnce: menyerahkan `receipt` ke pemanggil
let receipt = String::from("2 croissant, 1 kopi");
let hand_over = || receipt;
let customer_copy = hand_over();
println!("pelanggan menerima: {customer_copy}");
}

Output:

Selamat datang di Toko Roti Pojok!
Selamat datang di Toko Roti Pojok!
pesanan #1 dicatat
pesanan #2 dicatat
pelanggan menerima: 2 croissant, 1 kopi
▶ Coba sendiri

hand_over mengembalikan receipt itu sendiri, bukan salinannya. Begitu struknya diserahkan, closure itu tidak punya apa-apa lagi untuk diberikan. Itulah sebabnya ia FnOnce.

Ini gambaran yang perlu kamu ingat. Bayangkan tiga lingkaran, satu di dalam yang lain:

  • Lingkaran besar di luar adalah FnOnce. Setiap closure bisa dipanggil minimal sekali, jadi semua closure ada di sini.
  • Di dalamnya ada lingkaran yang lebih kecil, FnMut: closure yang bisa dipanggil berulang-ulang.
  • Tepat di tengah ada Fn: closure yang bisa dipanggil berulang-ulang tanpa mengubah apa pun.

Jadi setiap closure Fn juga merupakan FnMut dan FnOnce. Ini penting saat kamu menulis fungsi. Parameter yang meminta FnOnce menerima closure apa saja. Parameter yang meminta Fn adalah yang paling pilih-pilih.

Satu hal yang sering bikin kaget: keyword move tidak menentukan trait-nya. move || println!("{shop}") mengambil ownership (kepemilikan) atas shop, tapi hanya membacanya, jadi tetap Fn. Yang menentukan hanyalah isi closure.

Ada tiga cara menulis bound yang sama. Artinya persis sama, jadi pilih yang paling enak dibaca:

// 1. Parameter generic dengan bound
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2. `impl Fn` di posisi argumen: lebih pendek, tanpa perlu nama
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 3. Klausa `where`: rapi kalau bound-nya panjang
fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
scores.iter().map(|&s| f(s)).collect()
}
fn main() {
let bonus = 10;
let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50));
println!("{}", apply_twice(add_bonus, 50));
println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));
}

Output:

60
70
[50, 75, 100]
▶ Coba sendiri

Mungkin kamu bertanya-tanya, kok add_bonus bisa dikirim tiga kali? Closure yang hanya membaca tangkapannya murah untuk disalin, jadi Rust menyalinnya setiap kali.

Sekarang FnMut dan FnOnce. Dengan FnMut, parameter-nya butuh mut, karena memanggil closure itu mengubah isinya. Dengan FnOnce, kamu bisa memanggilnya sekali, dan selesai:

fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
for task in tasks {
on_done(task);
}
}
fn on_shutdown<F: FnOnce()>(cleanup: F) {
println!("mematikan sistem...");
cleanup();
}
fn main() {
let mut finished = Vec::new();
process_tasks(&["kirim email", "backup database"], |task| {
finished.push(task.to_string());
});
println!("selesai: {:?}", finished);
on_shutdown(|| {
// Loop ini mengambil ownership `finished`, jadi closure-nya FnOnce
for task in finished {
println!("diarsipkan: {task}");
}
});
}

Output:

selesai: ["kirim email", "backup database"]
mematikan sistem...
diarsipkan: kirim email
diarsipkan: backup database
▶ Coba sendiri

Aturan praktis yang bagus: minta trait yang paling longgar yang masih bisa dipakai fungsimu. Kalau kamu hanya memanggil closure sekali, minta FnOnce, dan pemanggil bisa mengirim closure apa saja. Kamu juga akan melihat ini di standard library: Option::map menerima FnOnce, sedangkan Iterator::map menerima FnMut.

Compiler menangkap setiap ketidakcocokan, dan pesannya di sini sangat membantu. Misalnya sebuah fungsi meminta Fn, tapi closure-mu mengubah sebuah penghitung:

fn run_report<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let mut runs = 0;
run_report(|| {
runs += 1;
println!("laporan ke-{runs}");
});
}
error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure
--> src/main.rs:9:9
|
1 | fn run_report<F: Fn()>(f: F) {
| - change this to accept `FnMut` instead of `Fn`
...
8 | run_report(|| {
| ---------- -- in this closure
| |
| expects `Fn` instead of `FnMut`
9 | runs += 1;
| ^^^^^^^^^ cannot assign

Artinya: runs tidak boleh diubah karena ia ditangkap oleh closure Fn. Compiler bahkan memberi tahu cara memperbaikinya: terima FnMut, lalu tambahkan mut pada parameternya. Dengan perubahan itu, program mencetak laporan ke-1 dan laporan ke-2:

fn run_report<F: FnMut()>(mut f: F) {
f();
f();
}

Kesalahan klasik lainnya adalah memanggil closure FnOnce dua kali:

fn main() {
let report = String::from("penjualan Q3");
let send = || {
let sent = report;
println!("mengirim {sent}");
};
send();
send();
}
error[E0382]: use of moved value: `send`
--> src/main.rs:8:5
|
7 | send();
| ------ `send` moved due to this call
8 | send();
| ^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `report` out of its environment
--> src/main.rs:4:20
|
4 | let sent = report;
| ^^^^^^

Catatan dari compiler menjelaskannya dengan gamblang: closure tidak bisa dipanggil lebih dari sekali karena panggilan pertama sudah menyerahkan report, jadi tidak ada yang tersisa untuk panggilan kedua. Kalau kamu benar-benar butuh dua panggilan, pinjam saja (let sent = &report;) dan closure-nya menjadi Fn. Kalau kamu mengirim closure seperti ini ke fungsi yang meminta Fn, kamu akan mendapat pesan serupa: error[E0507]: cannot move out of report, a captured variable in an Fn closure.

Sebuah fungsi juga bisa membuat closure lalu mengembalikannya. Kamu menulis tipe kembaliannya sebagai impl Fn(i32) -> i32, yang artinya “suatu tipe closure; kamu tidak perlu tahu namanya”:

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
move |x| x + amount
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_tip = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add_tip(20));
println!("{}", triple(7));
println!("{}", add_tip(triple(10)));
}

Output:

25
21
35
▶ Coba sendiri

Kenapa perlu move? Tanpa move, closure hanya akan meminjam amount. Padahal amount milik make_adder, dan ia hilang begitu make_adder selesai. Closure itu akan memegang referensi ke sesuatu yang sudah tidak ada. Rust menolaknya:

error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x + amount
| ^^^ ------ `amount` is borrowed here
| |
| may outlive borrowed value `amount`
|
help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + amount
| ++++

Pesan ini bilang closure-nya bisa hidup lebih lama daripada fungsinya, padahal ia meminjam amount, dan menyarankan keyword move. Dengan move, closure membawa salinan amount miliknya sendiri, seperti membawa bekal makan siangmu sendiri alih-alih menitipkannya di kulkas orang lain.

Closure dalam Box: banyak closure, satu tipe

Section titled “Closure dalam Box: banyak closure, satu tipe”

impl Fn mewakili satu tipe tersembunyi. Jadi kamu tidak bisa mengembalikan satu closure dari cabang if dan closure lain dari cabang else. Kamu juga tidak bisa menaruh closure yang berbeda-beda di Vec yang sama. Untuk kedua keperluan itu, masukkan setiap closure ke dalam box: Box<dyn Fn(f64) -> f64> artinya “closure apa saja dengan bentuk ini, disimpan di heap”. Idenya sama dengan halaman Trait object.

Berikut contoh kasir yang menerapkan daftar aturan harga bernama, secara berurutan:

fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
if express {
Box::new(move |total| total + fee * 2.0)
} else {
Box::new(move |total| total + fee)
}
}
fn main() {
let member_percent = 10.0;
let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
("diskon member", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))),
("kupon", Box::new(move |total: f64| total - coupon)),
("ongkir reguler", shipping_rule(false, 3.5)),
];
let mut total = 80.0;
println!("awal: {total:.2}");
for (name, rule) in &rules {
total = rule(total);
println!("setelah {name}: {total:.2}");
}
}

Output:

awal: 80.00
setelah diskon member: 72.00
setelah kupon: 67.00
setelah ongkir reguler: 70.50
▶ Coba sendiri

Box memerlukan sedikit alokasi di heap, dan setiap panggilan lewat sebuah pointer. Hal itu hampir tidak pernah jadi masalah. Pakai impl Fn kalau hanya ada satu tipe closure, dan Box<dyn Fn> kalau ada banyak.

Sebuah struct juga bisa menyimpan closure. Karena kamu tidak bisa menyebut nama tipe closure-nya, struct itu dibuat generic terhadap tipe tersebut. Berikut sebuah tombol kecil yang menjalankan kode saat diklik:

struct Button<F: Fn()> {
label: String,
on_click: F,
}
impl<F: Fn()> Button<F> {
fn click(&self) {
println!("[{}] diklik", self.label);
(self.on_click)();
}
}
fn main() {
let user = String::from("Ada");
let save = Button {
label: String::from("Simpan"),
on_click: move || println!("menyimpan draf milik {user}"),
};
save.click();
save.click();
}

Output:

[Simpan] diklik
menyimpan draf milik Ada
[Simpan] diklik
menyimpan draf milik Ada
▶ Coba sendiri

Perhatikan tanda kurung di (self.on_click)(). Tanda kurung itu memberi tahu Rust “ambil field-nya, lalu panggil”. Kalau tombolnya perlu mengubah tangkapannya setiap kali diklik, pakai F: FnMut() dan buat method-nya menerima &mut self.

Fungsi biasa juga bisa dikirim ke mana-mana. Tipenya ditulis dengan fn huruf kecil, seperti fn(i32) -> i32. Ini disebut function pointer (penunjuk fungsi), yaitu alamat biasa dari sepotong kode. Closure yang tidak menangkap apa pun juga bisa berubah menjadi function pointer:

fn double(x: i32) -> i32 {
x * 2
}
fn square(x: i32) -> i32 {
x * x
}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 {
op(x)
}
fn main() {
println!("{}", apply_op(double, 4));
println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1];
let mut value = 3;
for step in steps {
value = step(value);
}
println!("{value}");
}

Output:

8
3
37
▶ Coba sendiri

Semua function pointer punya satu tipe yang sama, jadi kamu bisa menaruhnya di array tanpa box. Kekurangannya, closure yang menangkap sesuatu tidak bisa menjadi function pointer. Compiler akan bilang: closures can only be coerced to fn types if they do not capture any variables (closure hanya bisa diubah menjadi tipe fn kalau tidak menangkap variabel apa pun). Kabar baiknya berlaku ke arah sebaliknya: setiap fungsi biasa juga mengimplementasikan Fn, FnMut, dan FnOnce, jadi kamu bisa mengirim double ke apply dari contoh sebelumnya. Itulah sebabnya kebanyakan kode memakai trait closure: trait itu menerima keduanya.

Lupa mut pada parameter FnMut. Memanggil FnMut mengubah closure itu, jadi variabelnya harus mut:

error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable
--> src/main.rs:3:9
|
3 | on_done(task);
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
| +++

Compiler tidak bisa meminjam on_done sebagai mutable karena ia tidak dideklarasikan dengan mut, dan ia menunjukkan tempat untuk menambahkannya.

Mengembalikan closure yang berbeda dari -> impl Fn. Dua closure berarti dua tipe, walaupun kelihatannya mirip:

fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
if express {
move |total| total + fee * 2.0
} else {
move |total| total + fee
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if express {
3 | | move |total| total + fee * 2.0
| | ------------------------------
| | |
| | the expected closure
| | expected because of this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected closure `{closure@src/main.rs:3:9: 3:21}`
found closure `{closure@src/main.rs:5:9: 5:21}`
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
|
1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new(move |total| total + fee * 2.0)
4 | } else {
5 ~ Box::new(move |total| total + fee)
|

Ikuti saran compiler: kembalikan Box<dyn Fn(f64) -> f64> dan bungkus setiap closure dengan Box::new(...).

Memanggil field closure seperti method. Kalau kamu menulis self.on_click(), Rust akan mencari method bernama on_click:

error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope
--> src/main.rs:9:14
|
9 | self.on_click();
| ^^^^^^^^ field, not a method
|
help: to call the type parameter stored in `on_click`, surround the field access with parentheses
|
9 | (self.on_click)();
| + +

Compiler bilang on_click adalah field, bukan method, dan menyarankan untuk membungkus akses field itu dengan tanda kurung.

Tulis fungsi ticket_dispenser(start: u32) yang mengembalikan closure. Setiap kali closure itu dipanggil, ia memberikan nomor antrean berikutnya: start, lalu start + 1, dan seterusnya. Buat dua mesin (toko roti mulai dari 100, apotek mulai dari 1) dan tunjukkan bahwa keduanya menghitung secara terpisah. Petunjuk: closure-nya mengubah penghitungnya sendiri, jadi tipe kembaliannya impl FnMut() -> u32, dan kamu akan butuh move.

Lihat solusi
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let ticket = next;
next += 1;
ticket
}
}
fn main() {
let mut bakery = ticket_dispenser(100);
let mut pharmacy = ticket_dispenser(1);
println!("toko roti: {}", bakery());
println!("toko roti: {}", bakery());
println!("apotek: {}", pharmacy());
println!("toko roti: {}", bakery());
}

Output:

toko roti: 100
toko roti: 101
apotek: 1
toko roti: 102
▶ Coba sendiri

Setiap mesin memindahkan next miliknya sendiri ke dalam closure, jadi keduanya tidak pernah berbagi penghitung. Variabel bakery dan pharmacy butuh mut karena keduanya FnMut.

Tulis retry(max_attempts, task). Fungsi ini memanggil task sampai hasilnya Ok, atau sampai sudah mencoba sebanyak max_attempts kali. Cetak satu baris untuk setiap percobaan yang gagal. Uji dengan closure yang gagal dua kali dengan pesan "server sibuk", lalu berhasil. Petunjuk: closure ujinya menghitung berapa kali ia dipanggil, jadi bound-nya FnMut() -> Result<String, String>, dan parameternya butuh mut.

Lihat solusi
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>
where
F: FnMut() -> Result<String, String>,
{
for attempt in 1..=max_attempts {
match task() {
Ok(value) => return Ok(value),
Err(e) => println!("percobaan {attempt} gagal: {e}"),
}
}
Err(String::from("menyerah"))
}
fn main() {
let mut calls = 0;
let result = retry(5, || {
calls += 1;
if calls < 3 {
Err(String::from("server sibuk"))
} else {
Ok(String::from("data berhasil diunduh"))
}
});
println!("{:?} setelah {calls} panggilan", result);
}

Output:

percobaan 1 gagal: server sibuk
percobaan 2 gagal: server sibuk
Ok("data berhasil diunduh") setelah 3 panggilan
▶ Coba sendiri

retry memanggil task lebih dari sekali, jadi FnOnce tidak cukup. Task-nya juga mengubah calls, jadi Fn pun tidak cocok. FnMut pas sekali. Saat retry selesai, closure-nya juga selesai, jadi main bisa membaca calls lagi.

  • Setiap closure punya tipe uniknya sendiri, jadi fungsi menerima closure lewat trait bound seperti F: Fn(i32) -> i32.
  • Fn hanya membaca tangkapannya, FnMut mengubahnya, dan FnOnce memindahkannya keluar sehingga hanya bisa dipanggil sekali. Yang menentukan adalah isi closure, bukan move.
  • Setiap Fn juga merupakan FnMut dan FnOnce. Minta trait paling longgar yang dibutuhkan fungsimu.
  • Tulis bound sebagai <F: Fn(..)>, impl Fn(..), atau di klausa where. Parameter FnMut butuh mut.
  • Kembalikan satu closure dengan -> impl Fn(..) (biasanya dengan move). Pakai Box<dyn Fn(..)> untuk closure yang berbeda-beda dari beberapa cabang atau di dalam Vec.
  • fn(i32) -> i32 adalah function pointer. Fungsi biasa dan closure yang tidak menangkap apa pun cocok dengan tipe ini.

Kuis singkat

1. Sebuah closure menjalankan total += price pada variabel yang ditangkapnya. Trait paling ketat apa yang diimplementasikannya?

2. Fungsimu memanggil parameter closure-nya tepat satu kali. Bound mana yang menerima closure paling banyak?

3. Kenapa make_adder(amount: i32) -> impl Fn(i32) -> i32 butuh move |x| x + amount?

4. Kamu ingin Vec yang berisi beberapa closure diskon yang berbeda-beda. Elemennya harus bertipe apa?

Skor: 0 / 4