Pencocokan Pola Tingkat Dalam

Destructuring struct dan enum serta binding @ pada rentang nilai.

Daftar isi

Topik ini membahas penguraian (destructuring) pada pola, yaitu kemampuan pola untuk memecah nilai majemuk menjadi bagian-bagiannya sekaligus. Pembahasan mencakup pola struct dan tuple yang mengikat field dengan nama, pola enum bertingkat yang menelusuri variant di dalam variant, penyingkatan .. untuk field yang diabaikan, serta binding @ yang mengikat nilai sekaligus mengujinya terhadap rentang. Setelah membaca topik ini, data bersarang tidak perlu diakses lapis demi lapis dengan titik dan match yang terpisah-pisah, melainkan cukup dinyatakan bentuknya sekali dalam satu pola.

Penguraian struct dan tuple#

Pola struct menuliskan nama tipenya beserta field yang dikehendaki di dalam kurung kurawal. Pola Titik { x, y } cocok dengan nilai Titik apa pun sekaligus mengikat kedua fieldnya ke nama x dan y. Pola seperti ini sah di banyak tempat, termasuk pada let, parameter fungsi, dan setiap lengan match, sehingga penguraian bukan kemampuan khusus match melainkan milik pola itu sendiri. Field pada pola juga boleh memuat pola lain, misalnya Titik { x: 0, .. } yang hanya cocok ketika field x bernilai nol, dan penulisan .. menyatakan bahwa field sisanya diabaikan.

rust destructuring-struct.rs
struct Titik {
    x: i32,
    y: i32,
}

fn deskripsi(titik: &Titik) -> &'static str {
    match titik {
        Titik { x: 0, y: 0 } => "tepat di titik asal",
        Titik { x: 0, .. } => "pada sumbu tegak",
        Titik { y: 0, .. } => "pada sumbu mendatar",
        Titik { .. } => "di luar kedua sumbu",
    }
}

fn main() {
    let a = Titik { x: 0, y: 0 };
    let b = Titik { x: 0, y: -3 };
    let c = Titik { x: 5, y: 0 };
    let d = Titik { x: 2, y: 7 };

    let Titik { x, y } = d;
    println!("d terurai menjadi x = {} dan y = {}", x, y);

    for titik in [&a, &b, &c, &d] {
        println!("Titik ({}, {}) berada {}", titik.x, titik.y, deskripsi(titik));
    }
}
text Keluaran
d terurai menjadi x = 2 dan y = 7
Titik (0, 0) berada tepat di titik asal
Titik (0, -3) berada pada sumbu tegak
Titik (5, 0) berada pada sumbu mendatar
Titik (2, 7) berada di luar kedua sumbu

Pada fungsi deskripsi, urutan lengan menentukan hasil karena titik asal juga memenuhi pola sumbu. Lengan pertama memakai dua pola nilai sekaligus, lengan kedua dan ketiga mengabaikan satu field dengan .., dan lengan terakhir menangkap semua sisanya. Fungsi deskripsi menerima referensi, sehingga pencocokan pola struct pada parameternya berjalan tanpa pemindahan kepemilikan. Pernyataan let Titik { x, y } = d; pada main juga tidak mengosongkan d karena field bertipe i32 yang menerapkan Copy, sehingga nilainya digandakan alih-alih dipindahkan. Penguraian tuple memakai bentuk yang lebih ringkas karena tanpa nama field.

Pola penguraian dapat hadir di setiap tempat yang menerima pola, bukan hanya pada match. Pernyataan let, kondisi if let dan while let, serta daftar parameter fungsi semuanya dapat memuat pola struct atau tuple. Sebuah fungsi dengan parameter (a, b): (i32, i32) menerima pasangan yang langsung terurai menjadi dua nama, sehingga pemanggil tidak perlu mengindeks nilainya di dalam badan fungsi. Kesatuan bentuk pola di semua tempat itu membuat penguraian menjadi kebiasaan yang konsisten, bukan fitur khusus satu konstruksi.

rust Fragmen: penguraian tuple
// tidak-dijalankan
let ukuran = (30, 20, 10);
let (panjang, lebar, tinggi) = ukuran;

Fragmen di atas tidak dijalankan karena bukan program utuh. Pola tuple mengikat setiap elemen menurut posisinya, dan pola ini lazim dipakai pada nilai kembalian fungsi yang mengembalikan beberapa hasil sekaligus. Elemen yang tidak diperlukan dapat ditulis _ atau disingkat dengan .. di posisi yang sesuai.

Penguraian enum bertingkat#

Variant enum yang membawa enum lain diurai dengan pola bertingkat. Seluruh tingkat harus tertulis lengkap dalam satu pola, sehingga Pesan::GantiWarna(Warna::Rgb(r, g, b)) menyatakan tiga hal sekaligus, yaitu variant luar GantiWarna, variant dalam Rgb, dan pengikatan ketiga angkanya. Kemampuan satu pola menelusuri beberapa lapis data sekaligus inilah yang dimaksud dengan pola tingkat dalam (deep pattern).

rust enum-bertingkat.rs
enum Warna {
    Rgb(u8, u8, u8),
    Abu(u8),
}

enum Pesan {
    GantiWarna(Warna),
    Geser(i32, i32),
}

fn jalankan(pesan: &Pesan) {
    match pesan {
        Pesan::GantiWarna(Warna::Rgb(r, g, b)) => {
            println!("Warna diatur ke RGB({}, {}, {})", r, g, b)
        }
        Pesan::GantiWarna(Warna::Abu(nuansa)) => {
            println!("Warna diatur ke abu-abu {}", nuansa)
        }
        Pesan::Geser(dx, dy) => println!("Kursor bergeser ({}, {})", dx, dy),
    }
}

fn main() {
    let daftar = [
        Pesan::GantiWarna(Warna::Rgb(80, 120, 200)),
        Pesan::GantiWarna(Warna::Abu(128)),
        Pesan::Geser(-4, 9),
    ];

    for pesan in &daftar {
        jalankan(pesan);
    }
}
text Keluaran
Warna diatur ke RGB(80, 120, 200)
Warna diatur ke abu-abu 128
Kursor bergeser (-4, 9)

Apabila pola dalam hanya menuliskan satu variant Warna, misalnya hanya Rgb, pencocokannya menjadi tidak lengkap dan kompilator menolak dengan E0004 karena variant Abu tidak tercakup. Kedalaman pola karenanya menuntut kelengkapan pada setiap tingkatnya, dan pesan errornya menyebutkan pola tingkat dalam yang belum tercakup sehingga letak kekurangannya langsung terlihat. Pola bertingkat juga dapat dicampur dengan binding untuk tingkat yang tidak perlu dibedah, misalnya Pesan::GantiWarna(warna) yang mengikat seluruh nilai Warna ke satu nama untuk diproses lebih lanjut di dalam blok lengannya. Campuran seperti ini berguna ketika tingkat dalam memiliki banyak variant tetapi hanya sebagian yang perlu perlakuan khusus. Variant sisanya cukup ditangani satu lengan _ pada match terpisah yang membedah nilai yang terikat pada nama tersebut.

Binding @ pada rentang nilai#

Binding @ mengikat nilai yang sedang dicocokkan sekaligus mengujinya terhadap pola di sebelah kanannya. Bentuknya nama @ pola, dan yang paling sering dipakai adalah rentang seperti n @ 0..=12, yang cocok apabila nilai berada pada rentang itu dan sekaligus mengikat nilainya ke n. Tanpa @, pola rentang hanya menguji keanggotaan nilai sehingga nilainya sendiri tidak dapat dipakai di hasil lengan.

rust binding-rentang.rs
enum Pesan {
    Halo { id: i32 },
}

fn sebut_usia(usia: u8) {
    match usia {
        n @ 0..=12 => println!("Anak berusia {} tahun", n),
        n @ 13..=17 => println!("Remaja berusia {} tahun", n),
        n @ 18..=64 => println!("Dewasa berusia {} tahun", n),
        n => println!("Lansia berusia {} tahun", n),
    }
}

fn main() {
    sebut_usia(9);
    sebut_usia(15);
    sebut_usia(34);
    sebut_usia(70);

    let masuk = Pesan::Halo { id: 10 };
    match masuk {
        Pesan::Halo { id: id @ 3..=7 } => println!("Id {} berada di rentang yang dicari", id),
        Pesan::Halo { id } => println!("Id {} di luar rentang", id),
    }
}
text Keluaran
Anak berusia 9 tahun
Remaja berusia 15 tahun
Dewasa berusia 34 tahun
Lansia berusia 70 tahun
Id 10 di luar rentang

Keempat lengan pada sebut_usia memakai rentang yang berdampingan tanpa tumpang tindih, sehingga satu lengan tanpa rentang di akhir menutup kelengkapan untuk usia di atas enam puluh empat. Binding @ juga bekerja di dalam pola struct, seperti Pesan::Halo { id: id @ 3..=7 } yang menguji field id terhadap rentang sekaligus mengikat nilainya. Rentang pada pola memakai ..= untuk rentang tertutup di kedua ujungnya, sama seperti notasi rentang pada ekspresi biasa.

Pengujian rentang sebenarnya juga dapat dilakukan lewat guard, misalnya lengan id if (3..=7).contains(&id). Perbedaannya ada pada pembacaan dan kelengkapan. Bentuk @ menyatakan rentang sebagai bagian dari pola sehingga terbaca berdampingan dengan pola lainnya, sedangkan guard memisahkan pengujian ke syarat tambahan yang berdiri sendiri, dan seperti telah dibahas pada topik pencocokan dengan match, guard tidak dihitung sebagai cakupan oleh pemeriksa kelengkapan. Rentang berdampingan yang lengkap menutup cakupan tanpa guard, sehingga bentuk @ menjadi pilihan yang lebih rapat untuk klasifikasi berbasis rentang seperti contoh di atas.

Kesalahan umum#

Kesalahan pertama adalah memakai .. pada pola struct padahal field yang diabaikan justru dibutuhkan di dalam blok lengan. Nama yang tidak disebut pola tidak terikat apa pun, sehingga kompilator menolak pemakaian nama itu dengan error resolusi nama. Solusinya adalah menuliskan field yang diperlukan secara eksplisit di samping .., karena penyingkatan hanya untuk field yang memang diabaikan. Perlu diketahui pula bahwa dalam daftar field pola struct, .. wajib berada di posisi akhir tanpa koma penutup, dan kompilator menyarankan pemindahan apabila .. tertulis di tengah. Berbeda dengan itu, pola tuple mengabaikan elemen menurut posisi, sehingga .. boleh berada di awal, di tengah, maupun di akhir untuk melompati elemen yang sesuai.

Kesalahan kedua adalah menganggap pola bertingkat hanya perlu menuliskan tingkat terluarnya. Pencocokan Pesan::GantiWarna(Warna::Rgb(..)) tanpa lengan untuk Warna::Abu ditolak karena kelengkapan berlaku pada setiap tingkat pola. Tuliskan seluruh kemungkinan pada tingkat dalam, atau ikat tingkat dalam dengan satu nama lalu bedah nilai itu dengan match terpisah apabila daftar polanya memang panjang. Pendekatan kedua menjaga tiap pencocokan tetap pendek dan mudah diperiksa satu per satu, dengan konsekuensi satu tingkat pemisahan antara pola luar dan pola dalam.

Ringkasan#

  • Pola struct mengikat field menurut nama, boleh memuat pola nilai pada field tertentu, dan memakai .. untuk mengabaikan sisanya.
  • Pola enum bertingkat menelusuri variant di dalam variant, dan kelengkapan pencocokan berlaku pada setiap tingkat.
  • Binding @ mengikat nilai sekaligus mengujinya terhadap rentang seperti n @ 0..=12, termasuk di dalam pola struct.
  • Penguraian berlaku umum pada let, parameter fungsi, dan lengan match, bukan hanya pada match.

Referensi#