Sejak Chapter pertama kita sudah memakai println! dan vec! tanpa pernah membahas tanda seru di ujung nama keduanya. Tanda seru tersebut menandai macro — fasilitas Rust untuk menulis kode yang pada saat kompilasi menulis kode lain. Fungsi telah menjadi alat utama kita sepanjang Course ini, tetapi ada kelompok kebutuhan yang secara struktural tidak bisa dijawab oleh fungsi: menerima jumlah argumen yang berubah-ubah, menerima bentuk panggilan yang bukan daftar argumen biasa, sampai menghasilkan definisi item baru dari ketikan yang ringkas. Section Macros dibuka dengan pertanyaan paling mendasar: kenapa bahasa yang sudah memiliki fungsi, generics, dan trait masih membutuhkan fasilitas tambahan bernama macro?
Fungsi Menolak Jumlah Argumen yang Berubah #
Setiap fungsi Rust memiliki tepat satu signature: jumlah parameter, tipe tiap parameter, dan tipe kembalian dikunci pada saat definisi. Kompilator kemudian memeriksa setiap pemanggilan terhadap signature tersebut. Perilaku ini menjaga keamanan tipe, tetapi sekaligus menutup pintu bagi antarmuka yang jumlah argumennya lentur. Misalkan kita menulis fungsi pencatat log untuk aplikasi kasir, lalu pada satu tempat pemanggil mengirim dua argumen karena ingin menyertakan angka:
fn catat(pesan: &str) {
println!("[CATAT] {}", pesan);
}
fn main() {
catat("Stok kopi tinggal 3 kg", 3);
}Kompilator menolak program di atas sebelum program sempat berjalan:
error[E0061]: this function takes 1 argument but 2 arguments were supplied
--> main.rs:6:5
|
6 | catat("Stok kopi tinggal 3 kg", 3);
| ^^^^^ - unexpected argument #2 of type `{integer}`
|
note: function defined here
--> main.rs:1:4
|
1 | fn catat(pesan: &str) {
| ^^^^^
help: remove the extra argument
|
6 - catat("Stok kopi tinggal 3 kg", 3);
6 + catat("Stok kopi tinggal 3 kg");
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0061`.Pesan E0061 menegaskan aturannya: fungsi catat menerima satu argumen, pemanggil mengirim dua. Bahasa C menjawab kebutuhan semacam ini dengan variadic function seperti printf, tetapi jalur tersebut menanggung risiko nyata — pencocokan antara penentu format (%s, %d) dan argumen hanya diperiksa saat program berjalan, sehingga satu salah ketik berubah menjadi crash di production. Rust tidak menyediakan variadic function untuk kode aplikasi biasa. Jawaban Rust atas kebutuhan jumlah argumen yang lentur adalah macro, dan jaminan pemeriksaannya tetap terjadi saat kompilasi.
Macro Diperluas Sebelum Type Check #
Mekanisme kuncinya sederhana: pemanggilan macro tidak pernah dieksekusi saat program berjalan. Saat kompilasi, kompilator mengganti setiap pemanggilan macro dengan hasil ekspansi — potongan kode Rust biasa — dan langkah penggantian ini terjadi sebelum fase type check. Setelah penggantian selesai, kompilator memeriksa kode hasil ekspansi persis seperti memeriksa kode yang kita tulis dengan tangan. Dua konsekuensi mengalir dari mekanisme ini. Pertama, satu macro bisa menerima pemanggilan dengan bentuk berbeda-beda karena penggantian dilakukan per pemanggilan. Kedua, hasil ekspansi boleh berupa item apa pun — termasuk definisi fungsi yang sebelumnya tidak ada, sesuatu yang mustahil dilakukan fungsi mana pun.
Program berikut mendemonstrasikan kedua konsekuensi tersebut sekaligus:
macro_rules! hello {
() => {
println!("Halo, selamat datang di Toko Kopi Nusantara!");
};
}
macro_rules! buat_fungsi {
($nama:ident, $nilai:expr) => {
fn $nama() -> u32 {
$nilai
}
};
}
buat_fungsi!(stok_awal, 12);
buat_fungsi!(nomor_meja, 7);
fn main() {
hello!();
println!("Stok awal hari ini: {} kg", stok_awal());
println!("Meja favorit pelanggan: nomor {}", nomor_meja());
}Output yang Dihasilkan:
Halo, selamat datang di Toko Kopi Nusantara!
Stok awal hari ini: 12 kg
Meja favorit pelanggan: nomor 7Karena macro bekerja di atas token, bukan nilai, cara membedahnya pun berbeda dari fungsi — kita bedah polanya:
- Baris 1-5 (definisi
hello!): macro memiliki satu arm (aturan) dengan pola()— pola kosong berarti pemanggilanhello!()tanpa argumen apa pun. Bagian setelah=>adalah ekspansi: kode pengganti yang berupa satu pemanggilanprintln!dengan teks tetap. - Baris 7-13 (definisi
buat_fungsi!): pola$nama:ident, $nilai:exprmenangkap dua potongan token dari pemanggilan — sebuah identifier dan sebuah ekspresi. Ekspansinya bukan sekadar statement, melainkan definisifnutuh yang identifier serta nilainya diisi dari hasil tangkapan pola. - Baris 15-16 (
buat_fungsi!(stok_awal, 12);): pemanggilan macro pada posisi item (di luar fungsi). Setelah ekspansi, kompilator melihat dua definisi fungsi biasa:fn stok_awal() -> u32 { 12 }danfn nomor_meja() -> u32 { 7 }— seolah kita menuliskannya dengan tangan. - Baris 19 (
hello!();): pemanggilan dengan nol argumen diganti menjadi pemanggilanprintln!tetap. Tidak ada fungsi Rust yang bisa dipanggil dengan nol argumen sekaligus banyak argumen, tetapi macro bisa punya beberapa arm untuk bentuk pemanggilan berbeda. - Baris 20-21:
stok_awal()dannomor_meja()dipanggil seperti fungsi biasa — memang fungsi biasa, hasil ekspansi macro yang telah lulus type check.
Alur lengkap dari kode sumber hingga binary dapat dipetakan sebagai berikut:
Diagram tersebut menempatkan macro pada posisi yang tepat: macro adalah tahap pengolahan teks kode yang berjalan paling awal di pipeline kompilasi, sebelum pemeriksaan tipe dan penerjemahan ke kode mesin.
Perbedaan Fundamental dengan Fungsi #
Tabel berikut merangkum garis pemisah antara fungsi dan macro:
| Aspek | Fungsi | Macro |
|---|---|---|
| Saat diproses | Saat program berjalan: pemanggilan melompat ke badan fungsi | Saat kompilasi: pemanggilan diganti hasil ekspansi |
| Jumlah argumen | Tetap, dikunci oleh satu signature | Lentur, ditangani arm dan repetisi |
| Bentuk argumen | Daftar ekspresi dengan tipe tetap | Urutan token bebas: identifier, operator, blok, bahkan item |
| Pemeriksaan tipe | Tiap argumen dicek terhadap parameter | Kode hasil ekspansi dicek sebagai kode Rust biasa |
| Keluaran | Nilai kembalian dengan satu tipe | Kode apa pun: statement, fungsi, struct, hingga blok impl |
Perlu dicatat bahwa kolom terakhir bukan berarti macro menjadi lubang keamanan. Macro sama sekali tidak menembus type check — kode yang dihasilkan macro harus tetap lolos pemeriksaan tipe sebelum masuk ke binary. Kelebihan macro murni terletak pada kemampuan menerima bentuk pemanggilan yang bervariasi dan kemampuan menghasilkan kode sebelum pemeriksaan tersebut berjalan. Sebaliknya, kelebihan tersebut dibayar dengan kelemahan nyata: pesan galat kadang menunjuk lokasi macro alih-alih kode kita, proses kompilasi memakan waktu lebih lama ketika macro digunakan meluas, dan pembacaan kode menuntut kita membayangkan hasil ekspansi di kepala.
Ringkasan #
Macro menjawab kebutuhan yang secara struktural tertutup bagi fungsi: fungsi mengunci jumlah dan tipe argumen pada satu signature sehingga pemanggilan dengan arity berbeda ditolak lewat E0061, sedangkan macro diperluas menjadi kode Rust biasa pada saat kompilasi — sebelum type check — sehingga satu macro bisa melayani bentuk pemanggilan berbeda dan bahkan melahirkan definisi fungsi baru. Jaminan tipe tidak hilang: hasil ekspansi tetap diperiksa kompilator seperti kode tulisan tangan. Dengan alasan keberadaan macro yang kini jelas, langkah berikutnya adalah membedah anatomi macro_rules! — arm, pola, dan ekspansi — serta melatihnya dengan macro pertama yang kita tulis sendiri.