Result & Option — Rust

Result & Option di Rust Rust tidak punya null atau exception. Sebagai gantinya, Rust menggunakan dua enum bawaan: Option dan Result untuk menangani ketiadaan…

Rust gak punya null dan gak punya exception. Sebagai gantinya ada dua enum bawaan: Option<T> untuk nilai yang mungkin gak ada, dan Result<T, E> untuk pekerjaan yang bisa gagal, lengkap dengan alasannya. Bayangin kasir Kedai Martabak Bang Rusdi. Pembeli menanyakan harga kopi. Kopi memang gak ada di menu: gak ada yang salah, jawabannya cuma "gak ada". Lain cerita dengan formulir pesanan yang kotak porsinya diisi "dua" pakai huruf. Kasir gak bisa membacanya sebagai angka, dan ia harus bilang kenapa: "porsinya bukan angka". Jawaban "ada, harganya 5000" atau "gak ada" itu Option: Some(5000) atau None. Jawaban "berhasil, 2 porsi" atau "gagal, karena ..." itu Result: Ok(2) atau Err(alasan).

🧪 Tebakan cepat: harga("es teh").unwrap() dicetak, lalu let kopi = harga("kopi").unwrap(); dan println!("Kopi: {}", kopi);. Apa yang terjadi? Tebak dulu, jawabannya ada di bagian "unwrap dan expect".

Option: mungkin ada, mungkin gak

Ini harga dari pelajaran enum, sekarang dengan es jeruk:

fn harga(menu: &str) -> Option<u32> {
    match menu {
        "martabak manis" => Some(32000),
        "martabak telur" => Some(38000),
        "es teh" | "es jeruk" => Some(5000),
        _ => None,
    }
}

fn main() {
    let es = harga("es teh");
    let kopi = harga("kopi");
    println!("{:?} {:?}", es, kopi);

    match kopi {
        Some(h) => println!("Kopi {}", h),
        None => println!("Kopi gak ada di menu"),
    }
    if let Some(h) = es {
        println!("Es teh {}", h);
    }
    println!("{}", kopi.unwrap_or(0));
    println!("{:?}", es.map(|h| h * 3));
    println!("{} {}", es.is_some(), kopi.is_none());
}
Some(5000) None
Kopi gak ada di menu
Es teh 5000
0
Some(15000)
true true

Result: berhasil, atau gagal dengan alasan

parse membaca teks jadi angka, dan bisa gagal:

fn main() {
    let isian = ["2", "dua", "", "5000000000"];
    for teks in isian {
        let porsi: Result<u32, _> = teks.parse::<u32>();
        println!("{:?}", porsi);
        match porsi {
            Ok(n) => println!("{} porsi", n),
            Err(e) => println!("Gagal: {}", e),
        }
    }
}
Ok(2)
2 porsi
Err(ParseIntError { kind: InvalidDigit })
Gagal: invalid digit found in string
Err(ParseIntError { kind: Empty })
Gagal: cannot parse integer from empty string
Err(ParseIntError { kind: PosOverflow })
Gagal: number too large to fit in target type

Option atau Result?

Pakai Option kalau "gak ada" itu hal biasa yang gak perlu dijelaskan: mencari harga di menu, pesanan.get(3), HashMap::get. Pakai Result kalau pekerjaannya bisa gagal dan pemanggilnya berhak tahu kenapa: membaca angka dari teks, membuka file, memanggil server. Keduanya bisa saling diubah: .ok_or(alasan) mengubah Option jadi Result dengan menambah alasan, dan .ok() mengubah Result jadi Option dengan membuang alasannya.

Fungsi yang memakai keduanya biasanya mengembalikan Result, karena kegagalannya perlu dijelaskan:

fn total(menu: &str, porsi: &str) -> Result<u32, String> {
    let h = match harga(menu) {
        Some(h) => h,
        None => return Err(format!("{} gak ada di menu", menu)),
    };
    match porsi.parse::<u32>() {
        Ok(p) => Ok(h * p),
        Err(e) => Err(format!("porsi \"{}\": {}", porsi, e)),
    }
}

total("martabak telur", "2") memberi Ok(76000), total("kopi", "2") memberi Err("kopi gak ada di menu"), dan total("es teh", "dua") memberi Err("porsi \"dua\": invalid digit found in string"). Pola "kalau gagal, langsung kembalikan error-nya ke pemanggil" ini sangat sering, jadi Rust punya singkatannya, operator ?. Itu bahan pelajaran berikutnya.

Mengolah tanpa match

fn main() {
    let menu = ["martabak manis", "martabak telur", "es teh", "kopi"];
    println!("{:?}", menu.get(2).and_then(|m| harga(m)));
    println!("{:?}", menu.get(3).and_then(|m| harga(m)));
    println!("{:?}", menu.get(9).and_then(|m| harga(m)));
    println!("{:?}", harga("kopi").ok_or("kopi gak ada di menu"));
    println!("{:?}", harga("es teh").ok_or("es teh gak ada di menu"));
    let porsi = "dua".parse::<u32>().unwrap_or(1);
    println!("{}", porsi);
    println!("{:?}", "2".parse::<u32>().ok());
}
Some(5000)
None
None
Err("kopi gak ada di menu")
Ok(5000)
1
Some(2)

Semua method ini juga ada di Result: map mengubah isi Ok, and_then menyambung langkah yang bisa gagal, unwrap_or memberi default.

unwrap dan expect

Ini kode tebakan tadi, dengan harga di atas:

fn main() {
    println!("Es teh: {}", harga("es teh").unwrap());
    let kopi = harga("kopi").unwrap();
    println!("Kopi: {}", kopi);
}
Es teh: 5000

thread 'main' (25517591) panicked at src/main.rs:12:30:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Lolos compile, Es teh: 5000 tercetak, lalu program panic (berhenti seketika) di unwrap() yang kedua. Itu jawaban tebakannya. unwrap() artinya "aku yakin isinya ada": pada Some(42) ia memberi 42, pada Ok(2) ia memberi 2, tapi pada None atau Err ia menghentikan program. Baris Kopi: gak pernah tercetak, dan cargo run keluar dengan kode 101. Angka di dalam kurung setelah 'main' adalah nomor thread, beda di tiap jalan.

expect sama dengan unwrap, tapi pesannya kamu tulis sendiri:

fn main() {
    let porsi: u32 = "dua".parse().expect("porsi harus angka");
    println!("{}", porsi);
}

thread 'main' (25518653) panicked at src/main.rs:2:36:
porsi harus angka: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Kenapa unwrap di kode production dianggap bau? Bayangin aplikasi kasir yang crash untuk semua orang cuma karena satu pembeli salah ketik porsi. unwrap wajar di contoh kecil, di test, dan di prototipe. Di kode sungguhan, tangani None dan Err dengan match, if let, atau method di atas. Kalau kamu benar-benar yakin sesuatu gak mungkin gagal, pakai expect dengan pesan yang menjelaskan alasannya, supaya kalau ternyata salah, pesannya langsung menunjuk masalahnya.

panic atau Result?

Panic untuk bug: janji di dalam programmu sendiri yang dilanggar, misalnya index di luar batas atau data yang "pasti ada" ternyata gak ada. Program berhenti, karena melanjutkan dengan data yang salah lebih berbahaya. Result untuk kegagalan yang wajar terjadi dari luar: isian pengguna, file yang hilang, jaringan yang putus. Pertanyaan pegangannya: bisakah pemanggil berbuat sesuatu yang masuk akal kalau ini gagal? Kalau bisa, kembalikan Result.

⚠️ Jebakan yang sering ketemu

TL;DR: Option<T> itu Some(nilai) atau None, untuk nilai yang mungkin gak ada (harga("kopi")). Result<T, E> itu Ok(nilai) atau Err(alasan), untuk pekerjaan yang bisa gagal ("dua".parse::<u32>() memberi Err(ParseIntError { kind: InvalidDigit }), yang dicetak {} jadi invalid digit found in string). Tangani dengan match atau if let, atau olah dengan unwrap_or, map, and_then, ok_or, dan ok. unwrap() memberi isinya (Some(42).unwrap() jadi 42) tapi panic pada None atau Err; expect("...") sama, dengan pesanmu sendiri. Panic untuk bug, Result untuk kegagalan yang wajar dari luar.

Yang akan kamu pelajari