Lewati ke konten

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 sini
42
dua dan 2
▶ Coba sendiri

Tidak 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.

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
▶ Coba sendiri

($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 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:

6
30
▶ Coba sendiri

$($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.

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:

7
9
2.5
▶ Coba sendiri

Untuk 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 * (dari sum!) artinya nol atau lebih. Aturan 2 butuh minimal satu nilai setelah koma, jadi panggilan max!(...) di dalamnya selalu punya sesuatu untuk dikerjakan.
  • max! bisa dipakai untuk bilangan bulat maupun pecahan, karena makro hanya menempelkan kode first > 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.

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:

apel
42
3.5
▶ Coba sendiri

Makro 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.

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]
▶ Coba sendiri

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.

  • 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

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