Fungsi #
Fungsi di Rust terlihat sederhana di permukaan, tapi ada beberapa aturan yang membedakannya dari bahasa lain. Parameter selalu butuh anotasi tipe eksplisit — tidak ada inferensi untuk parameter fungsi. Return value bisa implisit dari ekspresi terakhir tanpa titik koma, bukan hanya lewat return. Dan satu hal yang paling sering dilewatkan: interaksi fungsi dengan sistem ownership menentukan apakah kamu meneruskan nilai (move), meminjam (&T), atau meminjam secara mutable (&mut T), dan pilihan ini punya konsekuensi langsung pada cara pemanggil menggunakan datanya setelah memanggil fungsi. Artikel ini membahas semua dimensi fungsi di Rust — dari definisi dasar hingga closure, higher-order function, dan pola idiomatis yang membuat kode Rust ekspresif dan aman.
Definisi dan Anatomi Fungsi #
Fungsi di Rust dideklarasikan dengan kata kunci fn. Setiap parameter wajib memiliki anotasi tipe — berbeda dari variabel let yang bisa mengandalkan inferensi. Return type ditulis setelah ->.
// Fungsi tanpa parameter dan tanpa return value
fn sapa() {
println!("Halo dari Rust!");
}
// Fungsi dengan parameter — tipe selalu eksplisit
fn tambah(a: i32, b: i32) -> i32 {
a + b // expression tanpa ; = return value implisit
}
// Beberapa parameter dengan tipe berbeda
fn format_harga(nama: &str, harga: f64, diskon: u8) -> String {
let harga_akhir = harga * (1.0 - diskon as f64 / 100.0);
format!("{}: Rp{:.0} (diskon {}%)", nama, harga_akhir, diskon)
}
fn main() {
sapa();
println!("{}", tambah(5, 3));
println!("{}", format_harga("Kopi", 25_000.0, 10));
}
Return Value Implisit vs Eksplisit #
Ini salah satu hal yang paling membingungkan developer baru: ekspresi terakhir dalam fungsi tanpa titik koma adalah return value. Titik koma mengubah ekspresi menjadi statement — dan statement tidak punya nilai.
// BENAR: return implisit — ekspresi terakhir tanpa ;
fn kuadrat(x: i32) -> i32 {
x * x
}
// BENAR: return eksplisit — untuk early return
fn bagi_aman(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
return None; // early return — return eksplisit masuk akal di sini
}
Some(a / b) // return implisit di akhir
}
// ANTI-PATTERN: return eksplisit di akhir fungsi yang tidak perlu
fn kuadrat_buruk(x: i32) -> i32 {
return x * x; // return eksplisit di sini tidak salah, tapi berlebihan
}
// ANTI-PATTERN: titik koma yang tidak sengaja — fungsi return ()
fn kuadrat_rusak(x: i32) -> i32 {
x * x; // titik koma! ini jadi statement, bukan return value
// error[E0308]: mismatched types — expected i32, found ()
}
fn main() {
println!("{:?}", kuadrat(5)); // 25
println!("{:?}", bagi_aman(10.0, 3.0)); // Some(3.333...)
println!("{:?}", bagi_aman(10.0, 0.0)); // None
}
Unit Type ()
#
Fungsi tanpa return type secara implisit mengembalikan unit type () — tuple kosong. Ini bukan void seperti di C: () adalah nilai nyata yang bisa disimpan dalam variabel.
fn cetak_pesan(pesan: &str) {
println!("{}", pesan);
// secara implisit mengembalikan ()
}
fn main() {
// () bisa disimpan, tapi jarang berguna
let hasil: () = cetak_pesan("Halo");
println!("{:?}", hasil); // ()
}
Parameter: Ownership, Borrow, dan Mutable Borrow #
Pilihan bagaimana meneruskan argumen ke fungsi adalah keputusan desain, bukan sekadar sintaks. Tiga pilihan utama — move, borrow, mutable borrow — punya implikasi berbeda pada pemanggil.
flowchart TD
A{Fungsi perlu\nmengubah data?}
A -- Ya --> B["&mut T\nMutable borrow\nPemanggil tetap punya datanya"]
A -- Tidak --> C{Fungsi perlu\nmemiliki datanya?}
C -- Ya --> D["T (Move)\nOwnership berpindah\nPemanggil tidak bisa pakai lagi"]
C -- Tidak --> E{"Tipe besar\natau non-Copy?"}
E -- Ya --> F["&T\nImmutable borrow\nPaling sering digunakan"]
E -- Tidak --> G["T (Copy)\nCopy otomatis\nPemanggil tetap punya nilai"]// Move — fungsi mengambil ownership
fn konsumsi(s: String) -> String {
format!("Diproses: {}", s)
// s di-drop di sini karena fungsi punya ownership-nya
}
// Immutable borrow — fungsi hanya membaca
fn panjang(s: &str) -> usize {
s.len()
}
// Mutable borrow — fungsi mengubah data milik pemanggil
fn tambahkan_tanda_seru(s: &mut String) {
s.push('!');
}
fn main() {
let nama = String::from("Rust");
// Move: nama tidak bisa digunakan setelah ini
let hasil = konsumsi(nama);
// println!("{}", nama); // error: nama sudah di-move
println!("{}", hasil);
let kalimat = String::from("Halo dunia");
// Borrow: kalimat tetap valid
println!("Panjang: {}", panjang(&kalimat));
println!("Kalimat: {}", kalimat); // ✓ masih bisa dipakai
let mut ucapan = String::from("Halo");
// Mutable borrow: ubah ucapan dari dalam fungsi
tambahkan_tanda_seru(&mut ucapan);
println!("{}", ucapan); // "Halo!"
}
Memilih &str atau &String
#
Untuk parameter string, &str selalu lebih baik dari &String karena lebih fleksibel — ia menerima string literal, &String, dan slice sekaligus:
// ANTI-PATTERN: terlalu spesifik, hanya menerima &String
fn hitung_kata_buruk(teks: &String) -> usize {
teks.split_whitespace().count()
}
// BENAR: &str lebih generik
fn hitung_kata(teks: &str) -> usize {
teks.split_whitespace().count()
}
fn main() {
let owned = String::from("Rust adalah bahasa sistem");
let literal = "tiga kata saja";
println!("{}", hitung_kata(&owned)); // deref coercion: &String → &str
println!("{}", hitung_kata(literal)); // &str langsung
println!("{}", hitung_kata(&owned[5..])); // slice juga valid
}
Multiple Return Values via Tuple #
Rust tidak punya multiple return values secara langsung, tapi tuple berfungsi sama baiknya:
fn statistik(data: &[f64]) -> (f64, f64, f64) {
let n = data.len() as f64;
let rata: f64 = data.iter().sum::<f64>() / n;
let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
let maks = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(rata, min, maks) // return tiga nilai sekaligus
}
fn main() {
let nilai = [85.0, 92.0, 78.0, 95.0, 88.0];
let (rata, min, maks) = statistik(&nilai);
println!("Rata: {:.1}, Min: {}, Maks: {}", rata, min, maks);
}
Fungsi Generik #
Fungsi generik memungkinkan satu implementasi bekerja untuk banyak tipe. Compiler menghasilkan versi spesifik untuk setiap tipe yang digunakan — monomorphization — sehingga tidak ada overhead runtime.
// T harus bisa dibandingkan (PartialOrd) dan ditampilkan (Display)
fn cetak_terbesar<T: PartialOrd + std::fmt::Display>(daftar: &[T]) {
if daftar.is_empty() {
println!("Daftar kosong");
return;
}
let mut terbesar = &daftar[0];
for item in daftar {
if item > terbesar {
terbesar = item;
}
}
println!("Terbesar: {}", terbesar);
}
// Multiple type parameters
fn pertukaran<T: Clone, U: Clone>(a: T, b: U) -> (U, T) {
(b.clone(), a.clone())
}
// Where clause — lebih mudah dibaca untuk constraint yang panjang
fn proses<T, U>(nilai: T, transformasi: U) -> String
where
T: std::fmt::Debug + Clone,
U: Fn(T) -> String,
{
transformasi(nilai)
}
fn main() {
cetak_terbesar(&[3, 1, 4, 1, 5, 9, 2, 6]);
cetak_terbesar(&["apel", "mangga", "jeruk"]);
cetak_terbesar(&[3.14, 2.71, 1.41]);
let (b, a) = pertukaran(42, "halo");
println!("{} {}", a, b);
let hasil = proses(vec![1, 2, 3], |v| format!("{:?}", v));
println!("{}", hasil);
}
Closure #
Closure adalah fungsi anonim yang bisa menangkap variabel dari scope di sekitarnya. Sintaksnya menggunakan |parameter| ekspresi, jauh lebih ringkas dari fn biasa.
fn main() {
// Closure sederhana
let tambah = |a: i32, b: i32| a + b;
println!("{}", tambah(3, 4)); // 7
// Inferensi tipe — seringkali tidak perlu anotasi
let kuadrat = |x| x * x;
println!("{}", kuadrat(5i32)); // 25
// Closure multi-baris dengan blok
let proses = |data: &[i32]| {
let jumlah: i32 = data.iter().sum();
let rata = jumlah as f64 / data.len() as f64;
(jumlah, rata)
};
let angka = [10, 20, 30, 40, 50];
let (total, avg) = proses(&angka);
println!("Total: {}, Rata: {:.1}", total, avg);
}
Tiga Mode Capture Closure #
Closure bisa menangkap variabel dari lingkungannya dengan tiga cara berbeda, dipilih secara otomatis oleh compiler berdasarkan kebutuhan:
fn main() {
// 1. Capture by immutable borrow (&T) — default jika hanya membaca
let pesan = String::from("halo");
let cetak = || println!("{}", pesan); // meminjam pesan
cetak();
cetak();
println!("pesan masih valid: {}", pesan); // ✓
// 2. Capture by mutable borrow (&mut T) — jika closure mengubah nilai
let mut counter = 0;
let mut tambah_satu = || {
counter += 1; // mutable borrow dari counter
println!("Counter: {}", counter);
};
tambah_satu();
tambah_satu();
// println!("{}", counter); // error: masih borrowed oleh closure
drop(tambah_satu);
println!("Final counter: {}", counter); // ✓ setelah closure di-drop
// 3. Capture by move — dengan kata kunci `move`
let nama = String::from("Budi");
let sapa = move || println!("Halo, {}!", nama); // nama di-move ke closure
sapa();
// println!("{}", nama); // error: nama sudah di-move ke closure
// move wajib untuk closure yang hidup lebih lama dari scope asal
// (misalnya dikirim ke thread lain)
}
Trait Closure: Fn, FnMut, FnOnce
#
Rust membedakan closure berdasarkan bagaimana ia menggunakan variabel yang ditangkap:
| Trait | Kapan digunakan | Bisa dipanggil |
|---|---|---|
Fn | Hanya membaca atau tidak capture apapun | Berkali-kali |
FnMut | Mengubah variabel yang ditangkap | Berkali-kali (tapi butuh mut) |
FnOnce | Memindahkan variabel yang ditangkap | Hanya sekali |
// Fn — closure yang hanya membaca
fn panggil_dua_kali<F: Fn()>(f: F) {
f();
f(); // bisa dipanggil lebih dari sekali
}
// FnMut — closure yang mengubah state
fn panggil_dengan_mut<F: FnMut()>(mut f: F) {
f();
f();
}
// FnOnce — closure yang mengkonsumsi sesuatu
fn panggil_sekali<F: FnOnce() -> String>(f: F) -> String {
f() // hanya bisa dipanggil sekali
}
fn main() {
let x = 10;
panggil_dua_kali(|| println!("x = {}", x)); // Fn
let mut jumlah = 0;
panggil_dengan_mut(|| {
jumlah += 1;
println!("jumlah = {}", jumlah);
}); // FnMut
let nama = String::from("Rust");
let hasil = panggil_sekali(move || format!("Halo, {}!", nama)); // FnOnce
println!("{}", hasil);
}
Higher-Order Function #
Higher-order function adalah fungsi yang menerima fungsi lain sebagai parameter atau mengembalikan fungsi sebagai nilai. Ini adalah pola yang sangat umum di Rust, terutama bersamaan dengan iterator.
Fungsi sebagai Parameter #
Ada dua cara menerima fungsi sebagai parameter: dengan function pointer (fn) atau dengan trait bound closure (Fn/FnMut/FnOnce):
// Function pointer — hanya menerima fungsi biasa, bukan closure yang capture
fn terapkan(f: fn(i32) -> i32, nilai: i32) -> i32 {
f(nilai)
}
fn kali_dua(x: i32) -> i32 { x * 2 }
fn tambah_satu(x: i32) -> i32 { x + 1 }
// Trait bound — menerima fungsi biasa DAN closure
fn terapkan_closure<F: Fn(i32) -> i32>(f: F, nilai: i32) -> i32 {
f(nilai)
}
fn main() {
// Function pointer
println!("{}", terapkan(kali_dua, 5)); // 10
println!("{}", terapkan(tambah_satu, 5)); // 6
// Dengan closure
let faktor = 3;
println!("{}", terapkan_closure(|x| x * faktor, 5)); // 15 — closure capture faktor
// Fungsi bawaan sebagai function pointer
let angka = vec![1, -2, 3, -4, 5];
let positif: Vec<i32> = angka.iter()
.copied()
.filter(|x| x.is_positive())
.collect();
println!("{:?}", positif); // [1, 3, 5]
}
Fungsi Mengembalikan Closure #
Mengembalikan closure dari fungsi membutuhkan Box<dyn Fn...> karena ukuran closure tidak diketahui saat kompilasi:
// Kembalikan closure yang mengalikan dengan faktor tertentu
fn buat_pengali(faktor: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x * faktor)
}
// Kembalikan closure berdasarkan kondisi
fn pilih_operasi(operasi: &str) -> Box<dyn Fn(f64, f64) -> f64> {
match operasi {
"tambah" => Box::new(|a, b| a + b),
"kurang" => Box::new(|a, b| a - b),
"kali" => Box::new(|a, b| a * b),
_ => Box::new(|a, b| if b != 0.0 { a / b } else { f64::NAN }),
}
}
fn main() {
let kali_tiga = buat_pengali(3);
let kali_lima = buat_pengali(5);
println!("{}", kali_tiga(4)); // 12
println!("{}", kali_lima(4)); // 20
let tambah = pilih_operasi("tambah");
let kali = pilih_operasi("kali");
println!("{}", tambah(10.0, 5.0)); // 15
println!("{}", kali(10.0, 5.0)); // 50
}
Fungsi Rekursif #
Rust mendukung rekursi, tapi perlu diperhatikan: setiap pemanggilan rekursif menggunakan stack frame baru. Untuk rekursi dalam (jutaan level), gunakan iterasi atau tail call optimization secara manual.
// Faktorial rekursif — sederhana tapi risiko stack overflow untuk n besar
fn faktorial(n: u64) -> u64 {
match n {
0 | 1 => 1,
n => n * faktorial(n - 1),
}
}
// Fibonacci naif — sangat lambat karena banyak komputasi ulang
fn fib_naif(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
n => fib_naif(n - 1) + fib_naif(n - 2),
}
}
// Fibonacci dengan akumulator — lebih efisien (tail-call like)
fn fib_akumulator(n: u32, a: u64, b: u64) -> u64 {
match n {
0 => a,
_ => fib_akumulator(n - 1, b, a + b),
}
}
fn fib(n: u32) -> u64 {
fib_akumulator(n, 0, 1)
}
fn main() {
println!("10! = {}", faktorial(10)); // 3628800
println!("20! = {}", faktorial(20)); // 2432902008176640000
println!("fib(10) = {}", fib(10)); // 55
println!("fib(50) = {}", fib(50)); // 12586269025 — cepat
// fib_naif(50) akan sangat lambat
}
Diverging Function #
Fungsi yang tidak pernah kembali ke pemanggil memiliki return type ! (never type). Ini bukan void — ia berarti fungsi tersebut diverge: berakhir dengan panic!, loop tanpa akhir, atau keluar dari proses.
// Fungsi yang selalu panic
fn error_kritis(pesan: &str) -> ! {
eprintln!("ERROR KRITIS: {}", pesan);
panic!("{}", pesan);
}
// Fungsi yang loop selamanya
fn server_loop() -> ! {
loop {
// proses koneksi...
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
fn main() {
let input: Option<i32> = None;
// never type (!) kompatibel dengan tipe apapun
// ini valid karena ! bisa "menjadi" tipe apapun yang dibutuhkan
let nilai = match input {
Some(n) => n,
None => error_kritis("Input tidak boleh kosong"), // return type !
};
println!("{}", nilai);
}
Fungsi Bersarang #
Rust mengizinkan mendefinisikan fungsi di dalam fungsi. Fungsi bersarang tidak menangkap variabel dari scope luar (beda dari closure), tapi berguna untuk memecah logika lokal yang tidak relevan di luar:
fn proses_data(data: &[i32]) -> String {
// Fungsi helper lokal — hanya relevan di dalam proses_data
fn format_item(n: i32) -> String {
if n < 0 {
format!("({})", n.abs())
} else {
n.to_string()
}
}
fn adalah_prima(n: i32) -> bool {
if n < 2 { return false; }
(2..=(n as f64).sqrt() as i32).all(|i| n % i != 0)
}
data.iter()
.map(|&n| {
let label = if adalah_prima(n) { "*" } else { " " };
format!("{}{}", label, format_item(n))
})
.collect::<Vec<_>>()
.join(", ")
}
fn main() {
let data = [2, 3, -4, 5, 6, 7, -8, 11];
println!("{}", proses_data(&data));
// *2, *3, (4), *5, 6, *7, (8), *11
}
Ringkasan #
- Parameter selalu butuh anotasi tipe — tidak ada type inference untuk parameter fungsi, berbeda dari variabel
let.- Return implisit dari ekspresi terakhir — ekspresi tanpa titik koma di akhir fungsi adalah return value. Titik koma mengubahnya menjadi statement yang mengembalikan
().returneksplisit hanya untuk early return — gunakanreturnhanya untuk keluar lebih awal dari tengah fungsi, bukan di baris terakhir.- Pilih mode passing berdasarkan kebutuhan —
&Tuntuk baca-saja,&mut Tuntuk modifikasi,T(move) untuk fungsi yang butuh ownership.&strlebih fleksibel dari&Stringuntuk parameter string.- Closure menangkap environment — secara otomatis memilih borrow atau move berdasarkan penggunaan. Gunakan
moveuntuk closure yang perlu hidup melampaui scope asal (misalnya di thread).Fn/FnMut/FnOnce— tiga trait closure yang menentukan bisa dipanggil berapa kali.Fnpaling fleksibel,FnOncepaling restriktif.- Function pointer
fnvs trait boundFn— gunakan trait bound untuk menerima closure dan fungsi biasa sekaligus; gunakanfnjika hanya perlu fungsi biasa.- Fungsi mengembalikan closure butuh
Box<dyn Fn...>— karena ukuran closure tidak diketahui saat kompilasi.- Diverging function
-> !— fungsi yang tidak pernah return. Tipe!kompatibel dengan semua tipe, berguna dalammatcharm yang selalu panic atau loop selamanya.- Fungsi bersarang untuk logika lokal — beda dari closure: tidak menangkap variabel scope luar, berguna untuk memecah logika kompleks tanpa mengekspos helper ke scope lebih luas.