Sekarang saatnya menulis macro sendiri. Rust menyediakan macro_rules! — sistem macro declarative yang bekerja seperti match raksasa, tetapi objek yang dicocokkan bukan nilai melainkan urutan token dari pemanggilan macro. Satu macro dapat memiliki banyak arm; setiap arm terdiri dari pola di sisi kiri => dan ekspansi di sisi kanan; kompilator mencoba arm dari atas ke bawah dan memakai arm pertama yang polanya cocok. Chapter ini membahas anatomi lengkap macro_rules! melalui macro hello sederhana, macro say yang menerima argumen, dan macro hitung dengan beberapa arm sekaligus, lalu menutup dengan aturan pemakaian macro lintas modul.
Macro Pertama: Pola dan Ekspansi #
Kita mulai dari program utuh yang memuat tiga macro sekaligus. Ketiga macro didefinisikan sebelum fn main karena macro_rules! mengikuti scope tekstual: macro hanya dikenal kompilator setelah baris definisinya dibaca.
macro_rules! hello {
() => {
println!("Halo, selamat datang di Toko Kopi Nusantara!");
};
}
macro_rules! say {
($nama:expr) => {
println!("Halo, {}! Pesanan Anda siap diambil.", $nama);
};
}
macro_rules! hitung {
(total $harga:expr) => {
println!("Total belanja: Rp{}", $harga);
};
(diskon $harga:expr, $persen:expr) => {
println!(
"Diskon {}% dari Rp{} = Rp{}",
$persen,
$harga,
$harga * $persen / 100
);
};
}
fn main() {
hello!();
say!("Budi Santoso");
say!(String::from("Siti Rahma"));
hitung!(total 48000);
hitung!(diskon 48000, 10);
}Output yang Dihasilkan:
Halo, selamat datang di Toko Kopi Nusantara!
Halo, Budi Santoso! Pesanan Anda siap diambil.
Halo, Siti Rahma! Pesanan Anda siap diambil.
Total belanja: Rp48000
Diskon 10% dari Rp48000 = Rp4800Enam baris tersebut muncul dari enam pemanggilan macro dengan bentuk yang berbeda-beda — satu tanpa argumen, dua dengan satu ekspresi, dua dengan bentuk kata kunci plus nilai. Satu nama macro melayani semuanya. Sekarang mari kita bedah anatomi ketiga macro dengan fokus pada pola:
- Arm
() =>milikhello!(Baris 2-4): pola kosong cocok untuk pemanggilanhello!()tanpa argumen. Ekspansinya adalahprintln!dengan teks tetap — tidak ada bagian yang berubah antar pemanggilan, jadi setiaphello!()menghasilkan kode pencetakan yang sama persis. - Arm
($nama:expr) =>miliksay!(Baris 8-10):$namaadalah metavariable — wadah penampung hasil tangkapan pola. Fragmenexprmemerintahkan matcher memotong satu ekspresi utuh dari pemanggilan. Ekspansi menempatkan isi$namasebagai argumenprintln!. - Baris 29 (
say!("Budi Santoso")): matcher memotong ekspresi"Budi Santoso", ekspansi menjadiprintln!("Halo, {}! ...", "Budi Santoso"). - Baris 30 (
say!(String::from("Siti Rahma"))): fragmenexprmenangkap seluruh pemanggilanString::from("Siti Rahma")sebagai satu ekspresi, sehingga hasil ekspansi mencetakStringhasil konstruksi. Kecocokan tipe antaraStringdan{}tetap diperiksa kompilator pada kode hasil ekspansi. - Arm
(total $harga:expr)milikhitung!(Baris 14-16): pola memuat token harfiahtotaldiikuti metavariable. Pemanggilanhitung!(total 48000)cocok: tokentotalsama,$hargamenangkap48000. - Arm
(diskon $harga:expr, $persen:expr)(Baris 17-24): pola dengan token harfiahdiskon, dua metavariable, dan satu koma. Saat mencocokkanhitung!(diskon 48000, 10), matcher memotong48000sebagai$harga— pemotongan ekspresi berhenti di koma karena koma bukan kelanjutan ekspresi — lalu melewati koma, lalu menangkap10sebagai$persen. Ekspansi menghitung48000 * 10 / 100yang menghasilkan4800.
Arm Dicoba Berurutan seperti match #
Kompilator mencocokkan pemanggilan dengan arm secara berurutan dari definisi paling atas. Arm pertama yang cocok dipakai; arm di bawahnya diabaikan. Ketika tidak ada satu pun arm yang cocok, kompilasi gagal dengan pesan seperti error: no rules expected 'bonus' — pesan lengkapnya menampilkan token yang ditolak beserta arm mana saja yang sudah dicoba. Perilaku ini persis seperti match pada nilai: urutan arm menjadi bagian dari logika macro, sehingga arm yang paling spesifik sebaiknya ditempatkan di atas arm yang lebih umum.
Kemampuan menyusun banyak arm sekaligus inilah yang menjadikan macro semacam antarmuka pattern matching: hitung! menerima dua bentuk panggilan berbeda — satu kata kunci plus satu nilai, atau satu kata kunci plus dua nilai — dalam satu nama macro yang sama. Fungsi membutuhkan dua nama berbeda (hitung_total dan hitung_diskon) untuk hal yang sama.
Ekspansi Diperiksa seperti Kode Biasa #
Fakta bahwa macro diperluas sebelum type check sering disalahpahami sebagai jalan pintas untuk menembus pemeriksaan tipe. Program berikut membuktikan hal sebaliknya:
macro_rules! gabung {
($a:expr, $b:expr) => {
$a + $b
};
}
fn main() {
println!("{}", gabung!("kopi", "susu"));
}Setelah ekspansi, baris di dalam main menjadi println!("{}", "kopi" + "susu") — dan kode tersebut tidak sah karena operator + tidak didefinisikan untuk dua &str. Kompilator menolaknya:
error[E0369]: cannot add `&str` to `&str`
--> main.rs:3:12
|
3 | $a + $b
| ^ `+` cannot be used to concatenate two `&str` strings
...
8 | println!("{}", gabung!("kopi", "susu"));
| -----------------------
| | | |
| | | &str
| | &str
| in this macro invocation
|
= note: string concatenation requires an owned `String` on the left
= note: this error originates in the macro `gabung` (in Nightly builds, run with -Z macro-backtrace for more info)
help: create an owned `String` from a string reference
|
8 | println!("{}", gabung!("kopi".to_owned(), "susu"));
| +++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0369`.Pesan E0369 bahkan menyebut asal-usul kesalahan: galat bermula di dalam macro gabung, pada posisi ekspansi yang merujuk ke baris 3. Sama seperti kasus E0369 pada operasi penjumlahan biasa, perbaikannya adalah mengirim String milik (owned) di sisi kiri — saran .to_owned() pada potongan help bekerja persis seperti saran untuk kode tulisan tangan.
Macro lintas Modul dengan #[macro_export] #
Semua contoh sejauh ini memakai satu file, sehingga aturan scope tekstual cukup: macro didefinisikan di atas, dipakai di bawah. Untuk proyek multi-file, macro perlu diekspor dengan atribut #[macro_export]. Atribut tersebut menempatkan nama macro di root crate sehingga macro dapat dipanggil lewat path penuh atau diimpor dengan use. Simpan dua file berikut dalam satu direktori:
#[macro_export]
macro_rules! sapa_modul {
($nama:expr) => {
println!("Halo dari modul, {}!", $nama);
};
}mod util;
fn main() {
crate::sapa_modul!("Budi");
}Kompilasi dan jalankan dengan rustc:
rustc --edition 2021 main.rs
./mainOutput yang Dihasilkan:
Halo dari modul, Budi!File pertama bernama util.rs dan file kedua bernama main.rs. Tanpa #[macro_export], macro sapa_modul hanya dikenal di dalam util.rs setelah baris definisinya — pemanggilan sapa_modul! dari main.rs akan ditolak sebagai token tak dikenal. Dengan #[macro_export], nama macro ditempatkan di root crate, sehingga path crate::sapa_modul! sah dari modul mana pun; modul selain root juga bisa mengimpor macro dengan use crate::sapa_modul;. Inilah mekanisme yang dipakai macro standar: println! dapat dipanggil tanpa path karena macro tersebut diekspor dari crate standar dan bagian prelude.
Ringkasan #
macro_rules! mendefinisikan macro declarative sebagai kumpulan arm: pola di kiri => dicocokkan terhadap token pemanggilan secara berurutan, dan arm pertama yang cocok menentukan ekspansi di kanan =>. Metavariable seperti $harga:expr menangkap potongan token yang nantinya ditanam ke ekspansi; token harfiah seperti total membuat satu macro melayani beberapa bentuk pemanggilan. Kode hasil ekspansi tetap wajib lolos type check — E0369 pada macro gabung membuktikannya. Untuk pemakaian lintas modul, #[macro_export] menempatkan macro di root crate sehingga dapat dipanggil lewat crate::nama!. Satu batasan masih terasa: setiap arm hanya menampung jumlah argumen tetap. Kebutuhan daftar argumen sepanjang apa pun menjadikan tema Chapter berikutnya: repetisi dengan $(...),*.