Makro di Rust
Di halaman Sintaks, kamu sudah belajar patokan sederhananya: tanda ! berarti itu makro (macro), bukan fungsi biasa. Contohnya println!, vec!, format!. Halaman ini akhirnya menjelaskan apa sebenarnya artinya.
Kenapa println! tidak bisa jadi fungsi biasa
Section titled “Kenapa println! tidak bisa jadi fungsi biasa”Fungsi biasa seperti fn add(a: i32, b: i32) -> i32 selalu menerima jumlah argumen yang tetap dengan tipe yang tetap. Tapi kamu bisa memanggil println! dengan jumlah dan tipe argumen yang sangat beragam:
fn main() { println!("tidak ada argumen di sini"); println!("{}", 42); println!("{} dan {}", "dua", 2);}Output:
tidak ada argumen di sini42dua dan 2Tidak ada signature fungsi biasa yang bisa menerima semua panggilan itu. Makro bekerja dengan cara lain: makro dijalankan sebelum kodemu selesai di-compile, dan menghasilkan kode Rust sungguhan sesuai cara ia dipanggil. println!("{} dan {}", "dua", 2) pada dasarnya diekspansi menjadi kode nyata yang menangani tepat dua argumen itu. Setiap panggilan yang berbeda menghasilkan ekspansi yang berbeda.
Menulis makro sederhana
Section titled “Menulis makro sederhana”Kamu bisa mendefinisikan makro sendiri dengan macro_rules!. Ini contoh makro yang mengkuadratkan angka:
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { println!("{}", square!(5));}Output:
25($x:expr) artinya “terima satu ekspresi, dan namai $x”. Bagian => { ... } adalah kode hasil ekspansi makro itu. Di setiap tempat kamu menulis square!(5), compiler menggantinya dengan 5 * 5 sebelum benar-benar meng-compile apa pun.
Makro yang menerima banyak argumen
Section titled “Makro yang menerima banyak argumen”Makro bisa mencocokkan pola yang lebih rumit, termasuk daftar argumen yang berulang. Ini sesuatu yang sama sekali tidak bisa dilakukan fungsi biasa:
macro_rules! sum { ($($x:expr),*) => { { let mut total = 0; $(total += $x;)* total } };}
fn main() { println!("{}", sum!(1, 2, 3)); println!("{}", sum!(10, 20));}Output:
630$($x:expr),* artinya “nol atau lebih ekspresi, dipisahkan koma”. Pola yang sama inilah yang membuat vec![1, 2, 3] dan vec![1, 2, 3, 4, 5] sama-sama bisa dipakai dengan satu definisi makro.
Lebih dari satu aturan
Section titled “Lebih dari satu aturan”Sebuah makro bisa punya beberapa aturan (rule), dipisahkan titik koma. Rust mencobanya dari atas ke bawah dan memakai aturan pertama yang cocok. Sebuah aturan bahkan bisa memanggil makro itu lagi. Ini contoh max! yang bisa menerima berapa pun jumlah argumen:
macro_rules! max { // Aturan 1: hanya satu nilai. Yang terbesar adalah nilai itu sendiri. ($x:expr) => { $x }; // Aturan 2: satu nilai, lalu satu atau lebih nilai lain. ($x:expr, $($rest:expr),+) => { { let first = $x; let biggest_of_rest = max!($($rest),+); if first > biggest_of_rest { first } else { biggest_of_rest } } };}
fn main() { println!("{}", max!(7)); println!("{}", max!(3, 9, 4)); println!("{}", max!(2.5, 1.0));}Output:
792.5Untuk max!(3, 9, 4), aturan 1 tidak cocok (nilainya lebih dari satu), jadi aturan 2 yang dipakai. Aturan itu menyisihkan 3 dan mengekspansi max!(9, 4), yang kemudian mengekspansi max!(4). Yang terakhir ini cocok dengan aturan 1 dan berhenti di situ. Urutannya penting: Rust selalu mencoba aturan 1 lebih dulu.
Ada dua potong sintaks baru di sini:
+artinya satu atau lebih, sedangkan*(darisum!) artinya nol atau lebih. Aturan 2 butuh minimal satu nilai setelah koma, jadi panggilanmax!(...)di dalamnya selalu punya sesuatu untuk dikerjakan.max!bisa dipakai untuk bilangan bulat maupun pecahan, karena makro hanya menempelkan kodefirst > biggest_of_rest. Fungsi butuh generics untuk bisa melakukan itu.
expr bukan satu-satunya jenis hal yang bisa diterima makro. Beberapa jenis lain yang akan kamu lihat di makro sungguhan:
| Fragmen | Cocok dengan | Contoh |
|---|---|---|
expr |
ekspresi apa saja | 2 + 3, name.len() |
ident |
sebuah nama | count, Point |
ty |
sebuah tipe | i32, Vec<String> |
literal |
nilai literal | 42, "hai" |
Di mana kamu akan benar-benar bertemu makro
Section titled “Di mana kamu akan benar-benar bertemu makro”Kamu akan terus-menerus memakai makro (println!, vec!, format!, #[derive(Debug)]) tanpa pernah perlu menulis makro sendiri. Menulis makro kustom adalah keahlian khusus yang kebanyakan dipakai oleh pembuat library, yang perlu menghilangkan kode berulang (boilerplate) di codebase besar. Ada baiknya kamu tahu makro itu ada dan kira-kira bagaimana cara kerjanya, tapi ini bukan sesuatu yang perlu kamu pakai di awal belajar.
Latihan
Section titled “Latihan”1. Cetak semuanya
Section titled “1. Cetak semuanya”Tulis makro print_all! yang menerima berapa pun jumlah nilai, dipisahkan koma, dan mencetak masing-masing di barisnya sendiri. print_all!("apel", 42, 3.5) harus mencetak tiga baris. Petunjuk: cocokkan $($x:expr),* seperti di sum!, dan ulangi sebuah println! untuk setiap nilai dengan $( ... )*.
Lihat solusi
macro_rules! print_all { ($($x:expr),*) => { $(println!("{}", $x);)* };}
fn main() { print_all!("apel", 42, 3.5);}Output:
apel423.5Makro ini diekspansi menjadi tiga panggilan println! terpisah, satu untuk setiap argumen. Setiap nilai boleh punya tipe yang berbeda, dan itu tidak bisa ditangani fungsi biasa.
2. vec! buatanmu sendiri
Section titled “2. vec! buatanmu sendiri”Tulis my_vec! yang bekerja seperti vec!. my_vec![] menghasilkan Vec kosong, dan my_vec![1, 2, 3] menghasilkan Vec berisi elemen-elemen itu. Petunjuk: pakai dua aturan. Yang pertama tidak mencocokkan apa-apa, (). Yang kedua mencocokkan $($x:expr),+, membuat Vec::new(), lalu memasukkan setiap $x ke dalamnya dengan push.
Lihat solusi
macro_rules! my_vec { () => { Vec::new() }; ($($x:expr),+) => { { let mut v = Vec::new(); $(v.push($x);)+ v } };}
fn main() { let empty: Vec<i32> = my_vec![]; let numbers = my_vec![1, 2, 3]; println!("{:?} {:?}", empty, numbers);}Output:
[] [1, 2, 3]Kamu bisa memanggil makro apa pun dengan (), [] atau {}. Orang biasanya memakai kurung siku untuk makro berbentuk daftar seperti vec!, tapi my_vec!(1, 2, 3) juga bisa. Vec yang kosong butuh tipe : Vec<i32>, karena tanpa elemen apa pun Rust tidak bisa tahu isinya nanti bertipe apa.
Ringkasan
Section titled “Ringkasan”- Makro diekspansi menjadi kode sungguhan sebelum kompilasi. Karena itulah
println!bisa menerima jumlah dan tipe argumen yang berbeda setiap kali dipanggil. - Tanda
!setelah sebuah nama selalu menandakan kamu sedang memanggil makro, bukan fungsi. macro_rules!memungkinkanmu membuat makro sendiri, dengan mencocokkan pola seperti satu ekspresi ($x:expr) atau daftar berulang ($($x:expr),*).- Sebuah makro bisa punya beberapa aturan. Rust mencobanya dari atas ke bawah, dan sebuah aturan bisa memanggil makro itu lagi.
- Menulis makro adalah keahlian khusus. Memakai makro yang sudah ada di standard library sudah mencakup hampir semua yang kamu butuhkan.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa println! tidak bisa ditulis sebagai fungsi biasa?
2. Apa arti $x:expr di dalam pola macro_rules!?
3. Kapan makro diekspansi menjadi kode sungguhan?
Skor: 0 / 3
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Macros — The Rust Book (bahasa Inggris): bagaimana
vec!didefinisikan, plus pengenalan procedural macro seperti#[derive]. macro_rules!— Rust by Example (bahasa Inggris): contoh singkat yang bisa dijalankan tentang designator, overloading, dan pengulangan.- Macros By Example — The Rust Reference (bahasa Inggris): daftar lengkap jenis fragmen seperti
expr,identdantt, dan bagaimana tepatnya pencocokan bekerja. - The Little Book of Rust Macros (bahasa Inggris): pembahasan mendalam yang gratis dan ramah, untuk saat kamu ingin menulis makro yang lebih besar.