Fungsi yang kita tulis sejauh ini memilih jalur sederhana: satu kondisi khusus, satu jalur utama. Keputusan bisnis nyata jarang sesederhana itu. Menentukan nilai huruf dari angka rapor, menghitung ongkir berdasarkan zona pengiriman, atau menetapkan harga tiket berdasarkan usia — semuanya adalah fungsi dengan banyak cabang yang setiap cabangnya mengembalikan nilai berbeda. Kabar baiknya: Rust tidak hanya membiarkan pola ini, tetapi juga menjamin lewat kompiler bahwa tidak ada satu pun jalur yang lupa mengembalikan nilai. Pada Chapter penutup section ini kita akan menulis fungsi bercabang dengan if/else, match, dan return awal, lalu melihat bagaimana kompiler menangkap cabang yang bolong.

Fungsi dengan Rantai Cabang if/else #

Contoh klasiknya adalah konversi nilai angka menjadi nilai huruf. Empat kategori menuntut empat cabang, dan setiap cabang cukup menuliskan nilainya langsung karena if/else adalah expression:

rust
fn nilai_huruf(nilai: u32) -> char {
    if nilai >= 90 {
        'A'
    } else if nilai >= 80 {
        'B'
    } else if nilai >= 70 {
        'C'
    } else {
        'D'
    }
}

fn main() {
    println!("Nilai 95: {}", nilai_huruf(95));
    println!("Nilai 85: {}", nilai_huruf(85));
    println!("Nilai 75: {}", nilai_huruf(75));
    println!("Nilai 60: {}", nilai_huruf(60));
}

Output yang Dihasilkan:

text
Nilai 95: A
Nilai 85: B
Nilai 75: C
Nilai 60: D

Mari kita telaah bagaimana cabang-cabang tersebut bekerja:

  1. Baris 1 (fn nilai_huruf(nilai: u32) -> char {): Fungsi berjanji mengembalikan char — satu karakter. Janji ini berlaku untuk semua cabang, tanpa kecuali.
  2. Baris 2–9 (rantai if ... else if ... else): Seluruh rantai adalah satu expression besar. Setiap cabang berisi satu character literal seperti 'A' tanpa titik koma. Nilai cabang yang terpilih menjadi nilai seluruh rantai, sekaligus nilai kembalian fungsi karena rantai berada di baris terakhir.
  3. Cabang else di baris 8: Cabang penutup menampung semua nilai di bawah 70. Kehadiran else membuat rantai lengkap: setiap kemungkinan nilai pasti masuk ke salah satu cabang.
  4. Baris 14 (nilai_huruf(95)): Nilai 95 memenuhi syarat pertama (>= 90), cabang pertama terpilih, dan pemanggilan ini mengevaluasi menjadi 'A'. Pemanggilan lain menembus syarat pertama lalu berhenti di syarat yang sesuai.

Perhatikan urutan syarat. Rantai dievaluasi dari atas ke bawah, sehingga >= 80 yang ditulis setelah >= 90 secara efektif berarti “80 sampai 89”. Kalau kita membalik urutannya (>= 80 lebih dulu), nilai 95 akan berhenti di cabang 'B' dan tidak pernah sampai ke 'A'.

Kompiler Menjamin Setiap Jalur Bernilai #

Kekuatan Rust di pola ini terletak pada pemeriksaan kelengkapan. Kompiler menelusuri semua jalur eksekusi yang mungkin dan memastikan fungsi ber-tipe kembali -> char selalu berakhir dengan char. Coba kita sengaja buang cabang terakhir:

rust
fn harga_tiket(usia: u32) -> u32 {
    if usia < 3 {
        return 0;
    }

    if usia <= 12 {
        return 25000;
    }
}

fn main() {
    println!("Tiket: Rp{}", harga_tiket(30));
}

Kompilasi gagal dengan galat:

text
error[E0317]: `if` may be missing an `else` clause
 --> src/main.rs:6:5
  |
1 |   fn harga_tiket(usia: u32) -> u32 {
  |                                --- expected `u32` because of this return type
...
6 | /     if usia <= 12 {
7 | |         return 25000;
8 | |     }
  | |_____^ expected `u32`, found `()`
  |
  = note: `if` expressions without `else` evaluate to `()`
  = help: consider adding an `else` block that evaluates to the expected type

Pesan galat tersebut menunjukkan pola pikir kompiler: pengguna berusia di atas 12 tahun tidak masuk ke if mana pun, sehingga jalur “melewati kedua if” berakhir tanpa return. Jalur berakhir itu menghasilkan () — persis seperti pelajaran kita tentang expression tanpa nilai — padahal fungsi berjanji menghasilkan u32. Di bahasa lain, kasus ini bisa lolos kompilasi lalu meledak saat runtime; di Rust, kelalaian cabang tertangkap sebelum program pernah berjalan.

Banyak Jalur dengan match #

Ketika percabangan didasarkan pada nilai pasti (bukan rentang), match membuat tabel keputusan yang lebih rapi daripada rantai if. Fungsi ongkir berikut memetakan kode zona ke tarif:

rust
fn hitung_ongkir(kode_zona: u8) -> u32 {
    match kode_zona {
        1 => 10000,
        2 => 20000,
        3 => 35000,
        _ => 50000,
    }
}

fn main() {
    let zona = 2;
    println!("Ongkir zona {}: Rp{}", zona, hitung_ongkir(zona));
    println!("Ongkir zona 9: Rp{}", hitung_ongkir(9));
}

Output yang Dihasilkan:

text
Ongkir zona 2: Rp20000
Ongkir zona 9: Rp50000

Mari kita telaah struktur match pada kode di atas:

  1. Baris 2 (match kode_zona {): Kunci pencocokan adalah nilai parameter kode_zona. Kompiler akan membandingkan nilai tersebut dengan setiap pola.
  2. Baris 3–5 (1 => 10000, dst.): Setiap lengan pola menghubungkan satu nilai kunci (1, 2, 3) ke satu nilai hasil. Sama seperti cabang if, sisi kanan tanda => adalah expression bernilai.
  3. Baris 6 (_ => 50000,): Pola _ adalah catch-all yang menampung nilai lain di luar pola eksplisit. Pemanggilan hitung_ongkir(9) jatuh ke lengan ini.
  4. Kekuatan penegakan: match di Rust wajib lengkap (exhaustive). Menghapus lengan _ dari contoh ini membuat kompilasi gagal, karena u8 punya 256 kemungkinan nilai dan kompiler menuntut semuanya tercakup. Kelengkapan jalur return, yang di if/else dijaga lewat galat (), di match dijaga sejak desain.

Return Awal untuk Validasi Bertahap #

Gaya ketiga menggabungkan return awal dari Chapter Nilai Kembali dengan pemeriksaan berlapis. Setiap syarat khusus “menggaruk keluar” begitu terpenuhi, menyisakan nilai normal di baris terakhir:

rust
fn harga_tiket(usia: u32) -> u32 {
    if usia < 3 {
        return 0;
    }

    if usia <= 12 {
        return 25000;
    }

    if usia >= 60 {
        return 30000;
    }

    50000
}

fn main() {
    println!("Tiket usia 2: Rp{}", harga_tiket(2));
    println!("Tiket usia 10: Rp{}", harga_tiket(10));
    println!("Tiket usia 30: Rp{}", harga_tiket(30));
    println!("Tiket usia 65: Rp{}", harga_tiket(65));
}

Output yang Dihasilkan:

text
Tiket usia 2: Rp0
Tiket usia 10: Rp25000
Tiket usia 30: Rp50000
Tiket usia 65: Rp30000

Mari kita telaah alur fungsinya:

  1. Baris 2–4 (if usia < 3 { return 0; }): Bayi di bawah 3 tahun langsung mendapat tiket gratis dan eksekusi fungsi berhenti di sini; lima belas baris berikutnya tidak dibaca sama sekali.
  2. Baris 6–8 dan 10–12: Pola yang sama untuk anak-anak (tiket 25000) dan lansia (tiket 30000). Setiap return mengakhiri fungsi saat syaratnya terpenuhi.
  3. Baris 14 (50000): Ekspresi tanpa titik koma ini hanya tercapai kalau semua syarat khusus gagal — pengguna dewasa biasa. Kehadiran baris inilah yang membuat seluruh jalur fungsi lengkap dan kompilasi lolos.

Ketiga gaya tersebut — rantai if/else bernilai, tabel match, dan return awal berlapis — dapat dipilih sesuai bentuk masalah: rentang bersyarat cocok untuk if, nilai pasti cocok untuk match, dan syarat pengecualian cocok untuk return awal.

Ringkasan #

Fungsi bercabang ditulis dengan rantai if/else yang setiap cabangnya berupa expression, tabel match yang tiap lengan bernilai dan wajib lengkap, atau deretan return awal yang diakhiri nilai normal. Kompiler Rust menelusuri semua jalur eksekusi dan menolak fungsi yang memiliki jalur tanpa nilai kembali — galat () dan E0317 adalah penjaga pintu yang memastikan fungsi tidak pernah “sampai ujung tanpa membawa apa-apa”. Dengan penguasaan fungsi, scope, dan ekspresi, fondasi Rust kita sudah berdiri; section berikutnya menyelami kepemilikan data (ownership) yang menjadi ciri khas bahasa ini.