Macro hitung! pada Chapter sebelumnya mampu melayani dua bentuk pemanggilan, tetapi jumlah argumennya tetap dikunci per arm: arm total menampung satu nilai, arm diskon menampung dua nilai. Bayangkan kebutuhan yang dihadapi vec! — pemanggilan bisa berisi satu elemen, dua elemen, atau seratus elemen. Menulis satu arm untuk setiap panjang daftar jelas mustahil. Jawabannya adalah repetisi: pola $(...),* yang mencocokkan daftar token berulang, dipasangkan dengan $(...)* di sisi ekspansi yang mengulang kode penulisan sebanyak hasil tangkapan. Chapter ini membedah mekanisme tersebut lewat implementasi klasik my_vec! — versi mini dari vec! yang kita pakai sepanjang Course.

Masalah Daftar yang Panjangnya Tidak Tetap #

Mari kita rasakan dulu masalahnya. Tanpa repetisi, macro pembuat Vec terpaksa ditulis seperti berikut: satu arm untuk satu elemen, satu arm lagi untuk dua elemen, satu arm untuk tiga elemen, dan seterusnya hingga tak berujung. Setiap arm juga menghasilkan ekspansi berbeda — satu push, dua push, tiga push. Pendekatan tersebut mati jalan jauh sebelum mencapai pemanggilan dengan sepuluh elemen, dan pemanggil dengan sebelas elemen tetap ditolak.

Fungsi juga tidak menyelamatkan: fungsi menerima daftar argumen dengan jumlah tetap, dan Vec tidak bisa dibangun dari “daftar literal” tanpa memindahkan nilai satu per satu. Kebutuhan sebenarnya adalah menulis sekali — my_vec![15000, 22000, 18000] — lalu membiarkan macro menghasilkan kode push sebanyak yang dibutuhkan. Kebutuhan mengulang kode inilah yang dijawab repetisi macro.

Repetisi di Matcher dan di Ekspansi #

Sintaks repetisi punya dua sisi yang selalu berpasangan. Di sisi pola, $( ... ),* berarti: cocokkan bagian dalam kurung berulang kali, nol kali atau lebih, dipisah koma. Di sisi ekspansi, $( ... )* berarti: tulis ulang bagian di dalamnya sekali untuk setiap hasil tangkapan. Tanda bintang menyatakan “nol atau lebih”; tanda plus (+) menyatakan “satu atau lebih”. Koma di antara $(...) dan * pada pola adalah separator — token yang memisahkan tiap pengulangan.

Implementasi klasik my_vec! memperlihatkan kedua sisi tersebut:

rust
macro_rules! my_vec {
    ($($x:expr),*) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

fn main() {
    let daftar_harga = my_vec![15000, 22000, 18000];
    let total: i32 = my_vec![10, 20, 30].iter().sum();

    println!("Daftar harga: {:?}", daftar_harga);
    println!("Jumlah item: {}", daftar_harga.len());
    println!("Total pembelian: Rp{}", total);
}

Output yang Dihasilkan:

text
Daftar harga: [15000, 22000, 18000]
Jumlah item: 3
Total pembelian: Rp60

Mari kita bedah polanya arm per arm:

  1. Pola ($($x:expr),*) (Baris 2): bagian dalam $x:expr dicocokkan berulang. Pemanggilan my_vec![15000, 22000, 18000] menghasilkan tiga tangkapan untuk $x — 15000, 22000, dan 18000 — karena pemotongan tiap ekspresi berhenti di koma, koma dilewati sebagai separator, lalu ekspresi berikutnya dipotong.
  2. Blok pembungkus { ... } di ekspansi (Baris 3-10): ekspansi dibungkus blok supaya hasilnya berupa satu ekspresi bernilai Vec terakhir — pola yang sama dengan blok ekspresi pada Chapter Closure — sehingga my_vec! dapat dipakai di posisi ekspresi seperti sisi kanan let.
  3. let mut temp_vec = Vec::new(); (Baris 4): kode tulang punggung yang ditulis sekali. Bagian ini tidak berada dalam repetisi, jadi hanya muncul satu kali di hasil ekspansi berapa pun jumlah elemen.
  4. Ekspansi repetisi $( temp_vec.push($x); )* (Baris 5-7): bagian dalam repetisi ditulis ulang sekali per tangkapan $x. Untuk tiga elemen, kompilator menerima hasil ekspansi berikut — kode yang setara dengan yang ditulis tangan:
rust
{
    let mut temp_vec = Vec::new();
    temp_vec.push(15000);
    temp_vec.push(22000);
    temp_vec.push(18000);
    temp_vec
}
  1. Baris 14 (let daftar_harga = ...): ekspansi menghasilkan blok yang mengembalikan Vec<i32> berisi tiga harga, lalu nilai tersebut diikat ke daftar_harga.
  2. Baris 15 (my_vec![10, 20, 30].iter().sum()): hasil my_vec! adalah Vec biasa, sehingga method iter dan sum dapat dipanggil langsung pada hasil ekspansi sebelum diikat ke variabel.

Pemetaan antara repetisi pola dan repetisi ekspansi harus seimbang: jumlah kelompok tangkapan di pola menentukan jumlah pengulangan maksimum yang sah di ekspansi. Karena $x ditangkap sebagai satu kelompok berisi tiga nilai, ekspansi boleh memakai $x hanya di dalam satu repetisi yang sama.

Nol Repetisi, Trailing Comma, dan Arm Khusus #

Detail seputar tepi repetisi sering menjegal pemula, jadi kita petakan faktanya satu per satu. Pertama, tanda * mengizinkan nol pengulangan: pemanggilan my_vec![] sah dicocokkan (menghasilkan Vec kosong), meskipun ekspansi klasik di atas memicu peringatan unused_mut karena temp_vec tidak pernah diubah. vec! asli dari pustaka standar mengatasinya dengan arm khusus untuk kasus kosong — pola () => yang mengembalikan Vec::new() tanpa blok push. Kedua, tanda + menuntut minimal satu pengulangan: macro dengan pola ($($x:expr),+) menolak pemanggilan kosong dengan galat missing tokens in macro arguments. Tanda + pada praktiknya dipilih ketika macro memang tidak bermakna tanpa argumen — macro assertion atau pencatat pesan minimal satu item adalah contohnya. Ketiga, pola ($($x:expr),*) menolak koma penutup: pemanggilan m!(1, 2, 3,) gagal dicocokkan dengan galat unexpected end of macro invocation. Idiom untuk mengizinkan koma penutup adalah menambahkan $(,)? setelah repetisi — dan vec! asli memang menulis polanya sebagai ($($x:expr),+ $(,)?).

Perbedaan antara separator di pola dan pengulangan di ekspansi juga patut dicatat. Koma pada $( ... ),* hanya hidup di sisi pola sebagai pemisah antar tangkapan; sisi ekspansi $( ... )* pada contoh kita tidak memakai separator sama sekali karena setiap pengulangan sudah diakhiri titik koma. Ekspansi bisa juga memakai separator — misalnya $( $x ),* di dalam argumen println! — bila kode yang diinginkan berbentuk daftar dipisah koma.

Ringkasan #

Repetisi memutus keterikatan antara jumlah argumen dan jumlah arm: pola $(...),* mencocokkan daftar token dengan panjang bebas, dan $(...)* pada ekspansi menuliskan kode sepanjang daftar hasil tangkapan. Implementasi klasik my_vec! menunjukkan pasangan tersebut — tiga elemen pemanggilan menjadi tiga statement push di sekitar satu deklarasi temp_vec — lengkap dengan pembungkus blok agar hasil ekspansi berupa ekspresi bernilai Vec. Detail tepi yang terverifikasi: * menerima daftar kosong (dengan konsekuensi peringatan unused_mut pada ekspansi klasik), + menuntut minimal satu elemen, koma penutup butuh idiom $(,)?, dan $crate menjaga jalur tetap higienis lintas crate. Dengan arm dan repetisi di tangan, pertanyaan berikutnya menyentuh isi tangkapan itu sendiri: fragmen apa saja yang boleh menempati $x, dan bagaimana tiap fragmen memotong token — pokok bahasan Chapter Fragment Specifier.