Metavariable seperti $harga:expr terdiri dari dua bagian: nama dan fragment specifier — bagian setelah titik dua yang memberi tahu matcher cara memotong token pemanggilan. Pilihan specifier menentukan hasil tangkapan: expr menangkap satu ekspresi utuh, ident menangkap tepat satu identifier, ty menangkap satu tipe, dan seterusnya. Kesalahan memilih specifier adalah sumber kebingungan paling umum saat menulis macro — pola yang terlalu longgar menangkap potongan yang tidak diharapkan, sedangkan pola yang terlalu sempit menolak pemanggilan yang sah. Chapter ini mengulas lima fragment yang paling sering dipakai, memperagakan masing-masing dalam satu program teruji, lalu menutup dengan aturan follow set yang membatasi token apa yang boleh mengikuti sebuah fragment.
Fragment Menentukan Sejauh Mana Token Ditangkap #
Matcher macro bekerja di atas aliran token, dan fragment specifier adalah perintah pemotongan atas aliran tersebut. Saat matcher menemukan $v:ident, matcher mengambil tepat satu token identifier — pemotongan berhenti di situ, sekecil apa pun sisa token. Sebaliknya, saat matcher menemukan $e:expr, matcher menjalankan parser ekspresi penuh dan menelan token sejauh ekspresi masih sah — pemotongan harga * diskon + 5 menyatu sebagai satu tangkapan karena seluruh rangkaian tersebut adalah satu ekspresi. Derajat “lapar” inilah yang kita kendalikan lewat pemilihan specifier.
Rust menyediakan lebih dari sepuluh fragment — pat untuk pola match, path untuk jalur seperti Vec::new, item untuk definisi item, vis untuk modifier visibilitas seperti pub, tt untuk satu token tree, stmt untuk statement, lifetime, dan meta. Lima fragment berikut paling sering memenuhi kebutuhan macro aplikasi sehari-hari:
| Fragment | Menangkap | Contoh input yang cocok |
|---|---|---|
expr | Satu ekspresi utuh | 3 + 4, harga * 2, String::from("Teh") |
ident | Satu identifier | stok, hitung_diskon |
ty | Satu tipe | u32, Vec<String> |
literal | Satu nilai literal | "Promo", 25000, 'a' |
block | Satu blok { ... } | { let mut total = 0; total } |
Pemilihan fragment juga membatasi apa yang boleh masuk: literal menolak stok karena sebuah variabel bukan nilai literal — fragmen ini cocok untuk macro yang memang menuntut konstanta dari penggunanya. Fragment expr menjadi pilihan paling lentur sekaligus paling perlu diwaspadai, karena tangkapannya boleh berisi efek samping seperti pemanggilan fungsi atau pembuatan String baru.
Lima Fragment dalam Satu Program #
Program berikut mendefinisikan lima macro — satu per fragment — dengan toko kopi sebagai domain datanya:
macro_rules! pangkat_dua {
($e:expr) => {
{
let nilai = $e;
println!("({})^2 = {}", stringify!($e), nilai * nilai);
}
};
}
macro_rules! kenalkan_variabel {
($v:ident) => {
println!("Variabel '{}' bernilai {}", stringify!($v), $v);
};
}
macro_rules! inisialisasi {
($t:ty; $nilai:expr) => {
{
let variabel: $t = $nilai;
println!("Nilai awal bertipe {} adalah {}", stringify!($t), variabel);
}
};
}
macro_rules! label {
($l:literal) => {
println!("Literal tertangkap: {}", $l);
};
}
macro_rules! jalankan {
($b:block) => {
{
let hasil = $b;
println!("Block menghasilkan: {}", hasil);
}
};
}
fn main() {
pangkat_dua!(3 + 4);
let stok = 12;
kenalkan_variabel!(stok);
inisialisasi!(u32; 7);
label!("Diskon Hari Raya");
label!(25000);
jalankan!({
let mut total = 0;
for angka in 1..=10 {
total += angka;
}
total
});
}Output yang Dihasilkan:
(3 + 4)^2 = 49
Variabel 'stok' bernilai 12
Nilai awal bertipe u32 adalah 7
Literal tertangkap: Diskon Hari Raya
Literal tertangkap: 25000
Block menghasilkan: 55Mari kita bedah peran tiap fragment pada pola dan ekspansinya:
- Arm
($e:expr)milikpangkat_dua!(Baris 2-7): fragmentexprmenangkap3 + 4sebagai satu tangkapan. Ekspansi menahan nilai ke variabelnilaidulu sebelum dikuadratkan — trik untuk menghindari evaluasi$edua kali. Macro standarstringify!mengubah hasil tangkapan menjadi teks, sehingga output menampilkan(3 + 4)apa adanya. - Arm
($v:ident)milikkenalkan_variabel!(Baris 11-13): fragmentidenthanya menangkap nama variabelstok. Ekspansi mencetak nama lewatstringify!($v)dan nilainya lewat$v— identifier ditulis ulang di posisi ekspresi, lalu kecocokan tipestokdengan{}diperiksa kompilator pada hasil ekspansi. - Arm
($t:ty; $nilai:expr)milikinisialisasi!(Baris 17-22): fragmenttymenangkap satu tipeu32; fragmentexprmenangkap nilai7. Ekspansi menyusun deklarasilet variabel: u32 = 7;— tipe hasil tangkapan ditanam di posisi anotasi tipe. - Arm
($l:literal)miliklabel!(Baris 26-28): fragmentliteralmenangkap satu nilai literal. Pemanggilanlabel!("Diskon Hari Raya")danlabel!(25000)sama-sama cocok — string literal dan integer literal adalah literal, dan keduanya langsung dicetak tanpa evaluasi tambahan. - Arm
($b:block)milikjalankan!(Baris 32-37): fragmentblockmenangkap satu blok utuh berikut seluruh statement di dalamnya. Ekspansi menjadikan blok tersebut nilailet hasil = $b;— penjumlahan1..=10di dalam blok menghasilkan55, dan perhitungan tersebut terjadi di kode hasil ekspansi, bukan di dalam macro. - Baris 43 (
kenalkan_variabel!(stok);): variabelstokharus sudah ada di scope pemanggil — macro menulis ulang namastokpada kode pemanggil, bukan membawa nilai dari tempat lain. Sifat tulis-ulang-token inilah akar konsep macro hygiene.
Aturan Follow Set #
Pemotongan fragment yang “lapar” seperti expr menimbulkan pertanyaan parsing: setelah $a:expr menelan token sejauh mungkin, dari mana matcher tahu batas tangkapan ketika pola masih memiliki token lanjutan? Jawabannya, Rust membatasi token apa yang boleh mengikuti fragment di dalam pola — batasan ini bernama follow set. Fragment expr dan stmt hanya boleh diikuti =>, ,, atau ;. Pola yang melanggar batasan tersebut ditolak langsung saat definisi macro dikompilasi:
macro_rules! tambah {
($a:expr + $b:expr) => {
$a + $b
};
}
fn main() {
println!("{}", tambah!(2 + 3));
}Kompilator menolak program di atas saat memperluas pemanggilan tambah!, dengan galat yang menunjuk langsung ke baris definisi macro:
error: `$a:expr` is followed by `+`, which is not allowed for `expr` fragments
--> main.rs:2:14
|
2 | ($a:expr + $b:expr) => {
| ^ not allowed after `expr` fragments
|
= note: allowed there are: `=>`, `,` or `;`
error: aborting due to 1 previous errorFragment lain punya follow set masing-masing: ty dan path hanya boleh diikuti token seperti ,, =, ;, :, >, as, dan where; pat dibatasi =>, ,, =, if, dan in; fragment bebas seperti ident, literal, tt, dan block boleh diikuti token apa pun. Aturan ini terasa membatasi, tetapi balasannya pasti: satu pola selalu punya satu cara pemotongan.
Ringkasan #
Fragment specifier mengendalikan cara matcher memotong aliran token: ident mengambil satu identifier, literal satu nilai literal, expr satu ekspresi penuh, ty satu tipe, dan block satu blok utuh. Hasil tangkapan ditanam ke ekspansi sebagai token yang ditulis ulang, lalu diperiksa kompilator seperti kode biasa — stringify! menjadi alat bantu untuk melihat isi tangkapan saat men-debug pola. Follow set menjaga pemotongan tetap tak ambigu dengan membatasi token pengikut tiap fragment, dan tt menjadi jalan keluar ketika pola butuh fragment yang boleh diikuti token bebas. Dengan anatomi macro_rules! yang kini lengkap, Chapter berikutnya membalik arah pandang: kita bedah println!, vec!, dan format! — macro standar yang dari Chapter pertama diam-diam memakai seluruh mekanisme yang baru saja kita pelajari.