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);
}
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);
}
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.
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");
}
}
if let, else if, else if let, dan elseKalau 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}");
}
}
while let buat mencetak nilai-nilai selama rx.recv() mengembalikan OkContoh 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}");
}
}
for buat men-destructure sebuah tupleKode 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() {}
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.
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
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.