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

Semua Tempat di Mana Patterns Bisa Dipakai

Patterns muncul di berbagai tempat di Rust, dan kita sebenarnya udah sering sekali memakai mereka tanpa menyadarinya! Bagian ini membahas semua tempat di mana patterns itu valid untuk dipakai.

Lengan (Arms) dari match

Seperti yang sudah dibahas di Bab 6, kita memakai patterns di arms (lengan) dari ekspresi match. Secara formal, ekspresi match didefinisikan sebagai keyword match, sebuah nilai yang mau dicocokkan, dan satu atau lebih match arms yang terdiri dari sebuah pattern dan sebuah ekspresi buat dijalankan kalau nilainya cocok dengan pattern di arm tersebut, kayak gini:

match NILAI {
    PATTERN => EKSPRESI,
    PATTERN => EKSPRESI,
    PATTERN => EKSPRESI,
}

Misalnya, ini adalah ekspresi match dari Listing 6-5 yang mencocokkan sebuah nilai Option<i32> di dalam variabel x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

Patterns di ekspresi match ini adalah None dan Some(i) yang ada di sebelah kiri dari setiap tanda panah.

Satu persyaratan untuk ekspresi match adalah mereka harus bersifat exhaustive (menyeluruh/tuntas) yang berarti semua kemungkinan nilai buat ekspresi match tersebut harus ditangani (accounted for). Salah satu cara buat memastikan kita udah mencakup semua kemungkinannya adalah dengan memakai catch-all pattern (pola penangkap-semua) buat arm terakhirnya: misalnya, memakai nama variabel yang bakal cocok dengan nilai apa pun itu tidak bakal pernah gagal dan karena itu mencakup semua kasus yang tersisa.

Pattern spesifik _ bakal cocok dengan apa pun, tapi ia tidak pernah mengikat (bind) nilainya ke dalam sebuah variabel, jadi ia sering kali dipakai di match arm yang paling akhir. Pattern _ ini bisa berguna pas kita mau mengabaikan nilai apa pun yang tidak ditentukan (unspecified), sebagai contoh. Kita bakal membahas pattern _ lebih detail di “Mengabaikan Nilai di dalam sebuah Pattern” nanti di bab ini.

Statement let

Sebelum bab ini, kita cuma secara eksplisit ngebahas pemakaian patterns bersama match dan if let, tapi pada kenyataannya, kita udah memakai patterns di tempat lain juga, termasuk di dalam statement let. Misalnya, coba lihat pemberian nilai (variable assignment) langsung pakai let ini:

#![allow(unused)]
fn main() {
let x = 5;
}

Setiap kali kita memakai statement let kayak gini, kita sebenernya udah memakai patterns, biarpun kita mungkin tidak menyadarinya! Lebih formalnya, sebuah statement let itu kelihatannya kayak gini:

let PATTERN = EKSPRESI;

Di statement seperti let x = 5; di mana nama variabel ada di posisi PATTERN, nama variabel itu hanyalah bentuk yang paling sederhana dari sebuah pattern. Rust membandingkan ekspresi tersebut dengan pattern-nya lalu memberikan nilai (assigns) ke nama-nama yang ia temukan. Jadi, di contoh let x = 5;, x adalah sebuah pattern yang artinya “ikat (bind) apa pun yang cocok di sini ke variabel x.” Karena nama x adalah keseluruhan pattern-nya, pattern ini pada praktiknya berarti “ikat semuanya ke variabel x, apa pun nilainya.”

Buat ngelihat aspek pattern-matching (pencocokan pola) dari let dengan lebih jelas, coba lihat Listing 19-1, yang memakai sebuah pattern bareng let buat men-destructure (memecah) sebuah tuple.

fn main() {
    let (x, y, z) = (1, 2, 3);
}
Listing 19-1: Memakai pattern buat men-destructure sebuah tuple dan membikin tiga variabel sekaligus

Di sini, kita mencocokkan sebuah tuple terhadap sebuah pattern. Rust membandingkan nilai (1, 2, 3) dengan pattern (x, y, z) dan melihat kalau nilainya cocok dengan pattern tersebut, dalam arti dia melihat kalau jumlah elemennya sama di kedua sisinya, jadi Rust mengikat 1 ke x, 2 ke y, dan 3 ke z. Kita bisa membayangkan tuple pattern ini seolah-olah menyarangkan (nesting) tiga variable patterns individu di dalamnya.

Kalau jumlah elemen di dalam pattern-nya tidak cocok dengan jumlah elemen di dalam tuple-nya, tipe keseluruhannya tidak bakal cocok dan kita bakal dapat error compiler. Misalnya, Listing 19-2 menunjukkan sebuah percobaan buat men-destructure sebuah tuple yang berisi tiga elemen ke dalam dua variabel, yang mana tidak bakal jalan.

fn main() {
    let (x, y) = (1, 2, 3);
}
Listing 19-2: Membikin sebuah pattern yang salah di mana jumlah variabelnya tidak cocok dengan jumlah elemen di dalam tuple

Mencoba men-compile kode ini bakal menghasilkan type error (error tipe) ini:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Buat memperbaiki error-nya, kita bisa mengabaikan satu atau lebih nilai di dalam tuple tersebut memakai _ atau .., kayak yang bakal kita lihat di bagian “Mengabaikan Nilai di dalam sebuah Pattern”. Kalau masalahnya adalah kita punya terlalu banyak variabel di dalam pattern-nya, solusinya adalah mencocokkan (match) tipe-tipenya dengan membuang variabel-variabel tersebut sehingga jumlah variabelnya sama dengan jumlah elemen di tuple-nya.

Ekspresi Bersyarat (Conditional) if let

Di Bab 6, kita ngebahas gimana cara memakai ekspresi if let yang mana utamanya dipakai sebagai cara yang lebih singkat buat menulis bentuk ekuivalen (setara) dari sebuah match yang cuma mencocokkan satu kasus aja. Secara opsional, if let bisa dipasangkan dengan else yang berisi kode buat dijalankan kalau pattern di dalam if let tersebut tidak cocok.

Listing 19-3 menunjukkan kalau kita juga bisa mencampur dan mencocokkan (mix and match) ekspresi if let, else if, dan else if let. Ngelakuin hal ini ngasih kita fleksibilitas yang lebih besar ketimbang ekspresi match di mana kita cuma bisa mengekspresikan satu nilai aja buat dibandingkan dengan semua patterns-nya. Selain itu, Rust tidak mewajibkan agar kondisi-kondisi di dalam rangkaian lengan (arms) if let, else if, dan else if let itu saling berhubungan satu sama lain.

Kode di Listing 19-3 menentukan warna apa yang bakal dijadikan warna background (latar belakang) berdasarkan serangkaian pengecekan pada beberapa kondisi. Untuk contoh ini, kita sudah membikin variabel-variabel dengan nilai yang di-hardcode (ditulis langsung) yang mana di program sungguhan mungkin aja nilai-nilai ini didapat dari input user.

Filename: src/main.rs
fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}
Listing 19-3: Mencampur if let, else if, else if let, dan else

Kalau user menentukan sebuah warna favorit, warna itu bakal dipakai sebagai background. Kalau tidak ada warna favorit yang ditentukan dan hari ini adalah hari Selasa (Tuesday), warna background-nya adalah hijau (green). Selain itu, kalau user menentukan umurnya sebagai sebuah string dan kita bisa mem-parse-nya jadi sebuah angka dengan sukses, warnanya bakal jadi ungu (purple) atau oranye (orange) tergantung dari nilai angkanya. Kalau tidak ada satu pun dari kondisi ini yang berlaku, warna background-nya adalah biru (blue).

Struktur kondisional (bersyarat) ini membiarkan kita mendukung persyaratan yang kompleks. Dengan nilai-nilai hardcoded yang kita punya di sini, contoh ini bakal mencetak Using purple as the background color.

Kita bisa melihat kalau if let juga bisa memperkenalkan variabel baru yang menimpa (shadow) variabel yang sudah ada dengan cara yang sama kayak yang dilakukan sama match arms: baris if let Ok(age) = age memperkenalkan sebuah variabel age baru yang berisi nilai di dalam varian Ok-nya, menimpa variabel age yang sudah ada sebelumnya. Ini artinya kita harus menaruh kondisi if age > 30 di dalam blok tersebut: kita tidak bisa menggabungkan kedua kondisi ini jadi if let Ok(age) = age && age > 30. Nilai age baru yang mau kita bandingkan dengan 30 belum valid sampai scope (ruang lingkup) barunya dimulai bersamaan dengan tanda kurung kurawal pembuka.

Kelemahan dari memakai ekspresi if let adalah kalau compiler tidak bakal mengecek kelengkapannya (exhaustiveness), sedangkan dengan ekspresi match compiler bakal mengeceknya. Kalau kita kelupaan menaruh blok else terakhir dan oleh karenanya kelewatan (missed) buat menangani beberapa kasus, compiler tidak bakal memperingatkan kita soal kemungkinan adanya logic bug (kutu logika) tersebut.

Perulangan Bersyarat while let

Mirip dengan konstruksi if let, perulangan (loop) bersyarat while let memungkinkan sebuah loop while buat terus berjalan selama sebuah pattern masih terus cocok. Di Listing 19-4 kita menunjukkan sebuah loop while let yang menunggu pesan-pesan yang dikirim antar threads, tapi di kasus ini dia mengecek sebuah Result ketimbang sebuah Option.

fn main() {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }
}
Listing 19-4: Memakai loop while let buat mencetak nilai-nilai selama rx.recv() mengembalikan Ok

Contoh ini mencetak 1, 2, dan kemudian 3. Method recv mengambil pesan pertama dari sisi penerima (receiver side) saluran (channel) tersebut dan mengembalikan sebuah Ok(value). Saat pertama kali kita melihat recv di Bab 16, kita langsung meng-unwrap error-nya, atau berinteraksi dengannya layaknya sebuah iterator memakai sebuah loop for. Namun, seperti yang ditunjukkan Listing 19-4, kita juga bisa memakai while let, karena method recv mengembalikan sebuah Ok setiap kali ada pesan yang datang, selama si pengirimnya (sender) masih eksis, lalu mengembalikan sebuah Err begitu sisi pengirimnya memutuskan koneksi (disconnects).

Perulangan for

Di dalam loop for, nilai yang langsung mengikuti keyword for itu adalah sebuah pattern. Misalnya, di dalam for x in y, x itu adalah pattern-nya. Listing 19-5 mendemonstrasikan gimana cara memakai sebuah pattern di dalam loop for buat men-destructure (memecah belah) sebuah tuple sebagai bagian dari loop for tersebut.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{value} is at index {index}");
    }
}
Listing 19-5: Memakai sebuah pattern di dalam loop for buat men-destructure sebuah tuple

Kode di Listing 19-5 bakal mencetak yang berikut ini:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

Kita mengadaptasi sebuah iterator memakai method enumerate sehingga ia menghasilkan sebuah nilai beserta indeks buat nilai tersebut, yang ditaruh di dalam sebuah tuple. Nilai pertama yang dihasilkan adalah tuple (0, 'a'). Saat nilai ini dicocokkan dengan pattern (index, value), index bakal jadi 0 dan value bakal jadi 'a', lalu mencetak baris pertama dari outputnya.

Parameter Fungsi

Parameter dari sebuah fungsi juga bisa berupa patterns. Kode di Listing 19-6, yang mendeklarasikan fungsi bernama foo yang menerima satu parameter bernama x bertipe i32, harusnya sekarang udah terasa familier.

fn foo(x: i32) {
    // code goes here
}

fn main() {}
Listing 19-6: Sebuah signature fungsi yang memakai patterns di dalam parameternya

Bagian x itu adalah sebuah pattern lho! Sama kayak yang kita lakuin dengan let, kita bisa mencocokkan sebuah tuple di dalam argumen sebuah fungsi terhadap suatu pattern. Listing 19-7 memecah nilai-nilai di dalam sebuah tuple saat kita meneruskannya ke sebuah fungsi.

Filename: src/main.rs
fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}
Listing 19-7: Sebuah fungsi dengan parameter yang men-destructure sebuah tuple

Kode ini mencetak Current location: (3, 5). Nilai-nilai &(3, 5) itu cocok dengan pattern &(x, y), jadi x adalah nilai 3 dan y adalah nilai 5.

Kita juga bisa memakai patterns di dalam daftar parameter closure dengan cara yang sama seperti di dalam daftar parameter fungsi, karena closures itu mirip dengan fungsi, kayak yang udah dibahas di Bab 13.

Pada titik ini, kita udah melihat beberapa cara buat memakai patterns, tapi patterns tidak bekerja dengan cara yang sama persis di setiap tempat di mana kita bisa memakai mereka. Di beberapa tempat, patterns itu wajib bersifat irrefutable (tidak bisa dibantah/pasti sukses); di situasi lain, mereka bisa bersifat refutable (bisa dibantah/bisa gagal). Kita bakal ngebahas kedua konsep ini selanjutnya.