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.

rust
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:

text
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 = Rp4800

Enam 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:

  1. Arm () => milik hello! (Baris 2-4): pola kosong cocok untuk pemanggilan hello!() tanpa argumen. Ekspansinya adalah println! dengan teks tetap — tidak ada bagian yang berubah antar pemanggilan, jadi setiap hello!() menghasilkan kode pencetakan yang sama persis.
  2. Arm ($nama:expr) => milik say! (Baris 8-10): $nama adalah metavariable — wadah penampung hasil tangkapan pola. Fragmen expr memerintahkan matcher memotong satu ekspresi utuh dari pemanggilan. Ekspansi menempatkan isi $nama sebagai argumen println!.
  3. Baris 29 (say!("Budi Santoso")): matcher memotong ekspresi "Budi Santoso", ekspansi menjadi println!("Halo, {}! ...", "Budi Santoso").
  4. Baris 30 (say!(String::from("Siti Rahma"))): fragmen expr menangkap seluruh pemanggilan String::from("Siti Rahma") sebagai satu ekspresi, sehingga hasil ekspansi mencetak String hasil konstruksi. Kecocokan tipe antara String dan {} tetap diperiksa kompilator pada kode hasil ekspansi.
  5. Arm (total $harga:expr) milik hitung! (Baris 14-16): pola memuat token harfiah total diikuti metavariable. Pemanggilan hitung!(total 48000) cocok: token total sama, $harga menangkap 48000.
  6. Arm (diskon $harga:expr, $persen:expr) (Baris 17-24): pola dengan token harfiah diskon, dua metavariable, dan satu koma. Saat mencocokkan hitung!(diskon 48000, 10), matcher memotong 48000 sebagai $harga — pemotongan ekspresi berhenti di koma karena koma bukan kelanjutan ekspresi — lalu melewati koma, lalu menangkap 10 sebagai $persen. Ekspansi menghitung 48000 * 10 / 100 yang menghasilkan 4800.

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:

rust
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:

text
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:

rust
#[macro_export]
macro_rules! sapa_modul {
    ($nama:expr) => {
        println!("Halo dari modul, {}!", $nama);
    };
}
rust
mod util;

fn main() {
    crate::sapa_modul!("Budi");
}

Kompilasi dan jalankan dengan rustc:

bash
rustc --edition 2021 main.rs
./main

Output yang Dihasilkan:

text
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 $(...),*.