Control flow

See this chapter on YouTube: Part 1 and Part 2

Control flow artinya kita memberi tahu code untuk melakukan sesuatu pada kondisi yang berbeda. Control flow yang paling sederhana adalah if.

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("It's seven");
    }
}

Juga perlu dicatat bahwa Anda menggunakan == dan bukan =. == digunakan untuk membandingkan, = digunakan untuk assign (memberikan value pada sebuah variabel). Perhatikan juga bahwa kita menuliskan if my_number == 7 dan bukan if (my_number == 7). Anda tidak memerlukan bracket (tanda kurung) untuk pengkondisian if di Rust.

else if dan else akan memberikan Anda keleluasaan untuk membuat control flow yang lebih kompleks:

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("It's seven");
    } else if my_number == 6 {
        println!("It's six")
    } else {
        println!("It's a different number")
    }
}

Hasil cetaknya adalah It's a different number karena ia tidak sama dengan 7 ataupun 6.

Anda bisa menambahkan kondisi menggunakan && (and) dan || (or).

fn main() {
    let my_number = 5;
    if my_number % 2 == 1 && my_number > 0 { // % 2 artinya angka yang tersisa setelah variabelnya dibagi dengan 2 (modulus)
        println!("It's a positive odd number");
    } else if my_number == 6 {
        println!("It's six")
    } else {
        println!("It's a different number")
    }
}

Hasil printnya adalah It's a positive odd number karena di saat Anda membagi my_number dengan 2, maka akan tersisa 1, dan my_number lebih besar daripada 0.

Terlalu banyak if, else, dan else if bisa membuat code menjadi sulit untuk dibaca. Pada kasus seperti ini, Anda bisa menggunakan match (kecocokan), yang mana membuat code terlihat menjadi lebih bersih dan rapi. Hanya saja, Anda harus melihat setiap kemungkinan resultnya. Contohnya, code di bawah ini tidak akan berjalan:

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("it's zero"),
        1 => println!("it's one"),
        2 => println!("it's two"),
        // ⚠️
    }
}

Compiler akan memberikan pesan:

error[E0004]: non-exhaustive patterns: `3u8..=std::u8::MAX` not covered
 --> src\main.rs:3:11
  |
3 |     match my_number {
  |           ^^^^^^^^^ pattern `3u8..=std::u8::MAX` not covered

Ini artinya "Kamu beri tahu aku tentang kondisi 0 sampai dengan 2, tapi u8 bisa sampai dengan 255. Bagaimana tentang 3? Bagaimana tentang 4? Bagaimana tentang 5 dan seterusnya?". Jadinya Anda bisa menambahkan _ yang artinya "kondisi yang lainnya".

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("it's zero"),
        1 => println!("it's one"),
        2 => println!("it's two"),
        _ => println!("It's some other number"),
    }
}

Hasil printnya adalah It's some other number.

Ingatlah ini di saat Anda menggunakan match:

  • Anda menulis match dan kemudian membuat {} code block.
  • Tuliskan pattern pada sebelah kiri dan gunakan => (fat arrow) untuk menuliskan apa yang harus dilakukan apabila kondisinya matches (cocok).
  • Setiap baris di dalam block match disebut sebagai "arm".
  • Letakkan comma di antara setiap arm (bukan semicolon).

Anda bisa mendeklarasikan value dengan menggunakan match:

fn main() {
    let my_number = 5;
    let second_number = match my_number {
        0 => 0,
        5 => 10,
        _ => 2,
    };
}

second_number akan bernilai 10. Apakah Anda melihat keberadaan semicolon pada bagian akhir block match? Hal itu dikarenakan, setelah match telah selesai, kita sebenarnya memberitahukan kepada compiler hal ini: let second_number = 10;

Anda juga bisa menggunakan match pada hal-hal yang lebih rumit. Anda bisa menggunakan tuple untuk melakukannya.

fn main() {
    let sky = "cloudy";
    let temperature = "warm";

    match (sky, temperature) {
        ("cloudy", "cold") => println!("It's dark and unpleasant today"),
        ("clear", "warm") => println!("It's a nice day"),
        ("cloudy", "warm") => println!("It's dark but not bad"),
        _ => println!("Not sure what the weather is."),
    }
}

Hasil cetaknya adalah It's dark but not bad, karena value "cloudy" dan "warm" cocok dengan value dari sky dan temperature.

Bahkan Anda bisa menambahkan if di dalam match. Ini biasanya disebut sebagai "match guard":

fn main() {
    let children = 5;
    let married = true;

    match (children, married) {
        (children, married) if married == false => println!("Not married with {} children", children),
        (children, married) if children == 0 && married == true => println!("Married but no children"),
        _ => println!("Married? {}. Number of children: {}.", married, children),
    }
}

Hasilnya adalah Married? true. Number of children: 5.

Anda bisa menggunakan _ sebanyak yang Anda inginkan pada match. Pada match yang berhubungan dengan warna di bawah ini, kita memiliki 3 warna namun hanya diperiksa kecocokannya satu per satu.

fn match_colours(rbg: (i32, i32, i32)) {
    match rbg {
        (r, _, _) if r < 10 => println!("Not much red"),
        (_, b, _) if b < 10 => println!("Not much blue"),
        (_, _, g) if g < 10 => println!("Not much green"),
        _ => println!("Each colour has at least 10"),
    }
}

fn main() {
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colours(first);
    match_colours(second);
    match_colours(third);

}

Hasilnya adalah:

Not much blue
Each colour has at least 10
Not much green

Program di atas juga menunjukkan bagaimana statement match bekerja, karena pada contoh pertama ia hanya menampilkan Not much blue. Padahal variabel first juga tidak memenuhi syarat warna hijau (g == 0). Statement match selalu berhenti di saat ia menemukan kecocokan, dan tidak akan memeriksa sisanya. Ini juga adalah contoh yang baik dimana ia menunjukkan bahwa code dicompile dengan baik, hanya saja code ini berjalan tidak seperti yang kita inginkan.

Anda bisa membuat statement match yang lebih besar dan rumit untuk mengatasi masalah ini, tapi akan lebih baik jika kita menggunakan loop for. Kita akan membicarakan tentang loop di chapter selanjutnya.

Match harus me-return type yang sama. Jadinya Anda tidak bisa melakukan hal ini:

fn main() {
    let my_number = 10;
    let some_variable = match my_number {
        10 => 8,
        _ => "Not ten", // ⚠️
    };
}

Compiler akan memberikan pesan seperti ini:

error[E0308]: `match` arms have incompatible types
  --> src\main.rs:17:14
   |
15 |       let some_variable = match my_number {
   |  _________________________-
16 | |         10 => 8,
   | |               - this is found to be of type `{integer}`
17 | |         _ => "Not ten",
   | |              ^^^^^^^^^ expected integer, found `&str`
18 | |     };
   | |_____- `match` arms have incompatible types

Code di bawah ini juga tidak berjalan, dengan sebab yang sama seperti yang di atas:

fn main() {
    let some_variable = if my_number == 10 { 8 } else { "something else "}; // ⚠️
}

Akan tetapi bekerja dengan normal, karena ini bukan match sehingga Anda memiliki statement let yang berbeda setiap saat:

fn main() {
    let my_number = 10;

    if my_number == 10 {
        let some_variable = 8;
    } else {
        let some_variable = "Something else";
    }
}

Anda juga bisa menggunakan @ untuk memberikan nama ke value dari sebuah ekspresi match, dan Anda bisa menggunakannya. Pada contoh ini kita mencocokkan input i32 ke dalam function. Jika ia bernilai 4 atau 13, kita ingin menggunakan angka tersebut ke dalam statement println!. Sebaliknya, kita tidak perlu untuk menggunakannya.

fn match_number(input: i32) {
    match input {
    number @ 4 => println!("{} is an unlucky number in China (sounds close to 死)!", number),
    number @ 13 => println!("{} is unlucky in North America, lucky in Italy! In bocca al lupo!", number),
    _ => println!("Looks like a normal number"),
    }
}

fn main() {
    match_number(50);
    match_number(13);
    match_number(4);
}

Hasilnya adalah:

Looks like a normal number
13 is unlucky in North America, lucky in Italy! In bocca al lupo!
4 is an unlucky number in China (sounds close to 死)!