Fungsi

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:

TraitKapan digunakanBisa dipanggil
FnHanya membaca atau tidak capture apapunBerkali-kali
FnMutMengubah variabel yang ditangkapBerkali-kali (tapi butuh mut)
FnOnceMemindahkan variabel yang ditangkapHanya 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 ().
  • return eksplisit hanya untuk early return — gunakan return hanya untuk keluar lebih awal dari tengah fungsi, bukan di baris terakhir.
  • Pilih mode passing berdasarkan kebutuhan&T untuk baca-saja, &mut T untuk modifikasi, T (move) untuk fungsi yang butuh ownership. &str lebih fleksibel dari &String untuk parameter string.
  • Closure menangkap environment — secara otomatis memilih borrow atau move berdasarkan penggunaan. Gunakan move untuk closure yang perlu hidup melampaui scope asal (misalnya di thread).
  • Fn / FnMut / FnOnce — tiga trait closure yang menentukan bisa dipanggil berapa kali. Fn paling fleksibel, FnOnce paling restriktif.
  • Function pointer fn vs trait bound Fn — gunakan trait bound untuk menerima closure dan fungsi biasa sekaligus; gunakan fn jika 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 dalam match arm 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.

← Sebelumnya: Perulangan   Berikutnya: Struct →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact