Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control Flow yang Ringkas pake if let sama let else

Sintaks if let ngebolehin kita ngegabungin if sama let jadi cara yang lebih ringkas buat nge-handle nilai yang cocok sama satu pattern sambil nyuekin sisanya. Coba liat program di Listing 6-6 yang nge-match nilai Option<u8> di variabel config_max tapi cuma mau ngejalanin kode kalau nilainya itu varian Some.

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}
Listing 6-6: Sebuah match yang cuma peduli buat ngejalanin kode pas nilainya Some

Kalau nilainya Some, kita nyetak nilainya di varian Some dengan nge-bind nilai itu ke variabel max di dalem pattern-nya. Kita nggak mau ngelakuin apa- apa sama nilai None. Buat menuhin syarat ekspresi match, kita harus nambahin _ => () setelah memproses cuma satu varian, yang mana ini lumayan nyebelin karena jadi boilerplate code yang harus ditambahin.

Sebagai gantinya, kita bisa nulis ini dengan cara yang lebih singkat pake if let. Kode berikut perilakunya sama persis kayak match di Listing 6-6:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

Sintaks if let nerima sebuah pattern sama sebuah ekspresi yang dipisahin sama tanda sama dengan. Dia cara kerjanya sama kayak sebuah match, di mana ekspresinya dikasih ke match dan pattern-nya itu adalah arm pertamanya. Di kasus ini, pattern-nya adalah Some(max), dan max di-bind ke nilai di dalem Some. Terus kita bisa pake max di dalem body blok if let dengan cara yang sama kayak kita pake max di arm match yang terkait. Kode di dalem blok if let cuma jalan kalau nilainya cocok sama pattern-nya.

Pake if let artinya lebih dikit ngetik, lebih dikit indentasi, dan lebih dikit boilerplate code. Tapi, kita kehilangan pengecekan exhaustive (menyeluruh) yang diterapin sama match yang mastiin kalau kita nggak lupa nge-handle kasus apa pun. Milih antara match sama if let tergantung dari apa yang lagi kita lakuin di situasi kita saat itu dan apakah dapet keringkasan itu sebuah trade-off yang pas buat ngorbanin pengecekan menyeluruh.

Dengan kata lain, kita bisa mikirin if let sebagai syntax sugar buat sebuah match yang ngejalanin kode pas nilainya cocok sama satu pattern terus nyuekin semua nilai lainnya.

Kita bisa masukin sebuah else barengan sama if let. Blok kode yang ngikutin else itu sama kayak blok kode yang ngikutin kasus _ di ekspresi match yang setara sama if let dan else itu. Inget definisi enum Coin di Listing 6-4, di mana varian Quarter juga nyimpen nilai UsState. Kalau kita mau ngitung semua koin yang bukan quarter sambil nyebutin negara bagian dari si quarter, kita bisa lakuin itu pake ekspresi match, kayak gini:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

Atau kita bisa pake ekspresi if let dan else, kayak gini:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

Tetep di Jalur Aman (“Happy Path”) pake let...else

Pola yang umum adalah ngelakuin sebuah komputasi pas sebuah nilai ada isinya dan balikin nilai default kalau sebaliknya. Lanjut pake contoh kita soal koin dengan nilai UsState, kalau kita mau ngomong sesuatu yang lucu tergantung seberapa tua negara bagian di koin quarter itu, kita mungkin bakal nambahin sebuah method di UsState buat nge-cek umur sebuah negara bagian, kayak gini:

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Terus kita mungkin pake if let buat nge-match tipe koinnya, ngenalin variabel state di dalem body kondisinya, kayak di Listing 6-7.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-7: Nge-cek apakah sebuah negara bagian udah ada di tahun 1900 pake kondisional bersarang (nested) di dalem sebuah if let.

Emang beres sih kerjaannya, tapi ini ngegeser kerjaannya ke dalem body statement if let, dan kalau kerjaan yang harus dilakuin lebih ribet, bakal susah buat ngikutin persis gimana cabang-cabang top-level (tingkat atas)-nya berhubungan. Kita juga bisa manfaatin fakta kalau ekspresi ngasilin nilai buat ngasilin state dari if let atau return early (kembali lebih awal), kayak di Listing 6-8. (Kita juga bisa ngelakuin hal yang mirip pake match.)

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-8: Pake if let buat ngasilin sebuah nilai atau return early.

Tapi ini agak nyebelin buat diikutin dengan caranya sendiri! Satu cabang dari if let ngasilin nilai, dan yang satunya return dari fungsi sepenuhnya.

Buat bikin pola umum ini lebih enak buat diekspresikan, Rust punya let...else. Sintaks let...else nerima sebuah pattern di sisi kiri dan sebuah ekspresi di sisi kanan, mirip sekali sama if let, tapi dia nggak punya cabang if, cuma cabang else. Kalau pattern-nya cocok, dia bakal nge-bind nilai dari pattern ke scope luar. Kalau pattern-nya nggak cocok, programnya bakal ngalir ke dalem arm else, yang harus return (kembali) dari fungsinya.

Di Listing 6-9, kita bisa liat gimana penampakan Listing 6-8 pas pake let...else sebagai ganti dari if let.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-9: Pake let...else buat ngejelasin alur (flow) lewat fungsinya.

Perhatiin ya kalau dia tetep “on the happy path” (di jalur aman yang diharapkan) di body utama fungsinya pake cara ini, tanpa harus punya alur kontrol yang bener-bener beda jauh buat dua cabang kayak yang dilakuin sama if let.

Kalau kita ada di situasi di mana program kita punya logika yang terlalu panjang (verbose) buat diekspresikan pake match, inget ya kalau if let sama let...else juga ada di dalem toolbox Rust kita.

Ringkasan

Kita sekarang udah ngebahas gimana cara pake enum buat bikin tipe kustom yang bisa jadi salah satu dari sekumpulan nilai yang di-enumerate. Kita udah nunjukin gimana tipe Option<T> bawaan standard library ngebantu kita pake sistem tipe buat nyegah error. Pas nilai enum punya data di dalemnya, kita bisa pake match atau if let buat ngekstrak dan pake nilai-nilai itu, tergantung dari seberapa banyak kasus yang perlu kita handle.

Program Rust kita sekarang bisa mengekspresikan konsep di domain kita pake struct dan enum. Bikin tipe kustom buat dipake di API kita mastiin keamanan tipe (type safety): compiler bakal mastiin fungsi kita cuma dapet nilai dari tipe yang diharapkan sama tiap fungsinya.

Buat nyediain API yang terorganisir dengan baik ke user kita yang gampang buat dipake dan cuma nge-ekspos tepat apa yang dibutuhin sama user kita aja, yuk sekarang kita beralih ke modules (modul) di Rust.