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:
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:
Daftar harga: [15000, 22000, 18000]
Jumlah item: 3
Total pembelian: Rp60Mari kita bedah polanya arm per arm:
- Pola
($($x:expr),*)(Baris 2): bagian dalam$x:exprdicocokkan berulang. Pemanggilanmy_vec![15000, 22000, 18000]menghasilkan tiga tangkapan untuk$x—15000,22000, dan18000— karena pemotongan tiap ekspresi berhenti di koma, koma dilewati sebagai separator, lalu ekspresi berikutnya dipotong. - Blok pembungkus
{ ... }di ekspansi (Baris 3-10): ekspansi dibungkus blok supaya hasilnya berupa satu ekspresi bernilaiVecterakhir — pola yang sama dengan blok ekspresi pada Chapter Closure — sehinggamy_vec!dapat dipakai di posisi ekspresi seperti sisi kananlet. 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.- 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:
{
let mut temp_vec = Vec::new();
temp_vec.push(15000);
temp_vec.push(22000);
temp_vec.push(18000);
temp_vec
}- Baris 14 (
let daftar_harga = ...): ekspansi menghasilkan blok yang mengembalikanVec<i32>berisi tiga harga, lalu nilai tersebut diikat kedaftar_harga. - Baris 15 (
my_vec![10, 20, 30].iter().sum()): hasilmy_vec!adalahVecbiasa, sehingga methoditerdansumdapat 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.