macro_rules! în Rust

Definește un macro propriu: cod care scrie cod la compilare, după tipare. Așa sunt construite println!, vec! sau assert_eq!.

Sintaxă
macro_rules! nume {
    (tipar1) => { cod generat };
    ($x:expr, $y:expr) => { ... };
    ($($element:expr),* $(,)?) => { ... $( ... $element ... )* };
}

Ce face macro_rules!

Un macro declarativ primește tokeni (bucăți de cod), îi compară cu tiparele din ramuri, de sus în jos, și înlocuiește apelul cu codul din prima ramură potrivită. Se apelează cu !: salut!(), maxim!(3, 7, 5). Spre deosebire de o funcție, poate primi un număr variabil de argumente și poate genera orice cod (funcții, structuri, implementări).

În tipare, $nume:fel capturează o bucată de cod de un anumit fel: expr (expresie), ident (nume), ty (tip), tt (un token), literal, pat, block. Repetițiile se scriu $( ... ),* (zero sau mai multe, separate prin virgulă) sau $( ... ),+ (cel puțin una); în codul generat, $( ... )* repetă pentru fiecare element capturat. $(,)? permite o virgulă la final.

Un macro trebuie definit înainte de locul unde este folosit în fișier. Pentru a-l folosi din alt modul sau crate se marchează cu #[macro_export]. Folosește macro-uri doar când o funcție nu ajunge (număr variabil de argumente, cod repetitiv pentru mai multe tipuri); erorile din macro-uri sunt mai greu de înțeles.

Exemple

Exemplul 1: Macro cu mai multe ramuri și număr variabil de argumente

maxim! are două ramuri: una pentru un singur argument și una recursivă pentru mai multe. Funcționează cu orice tip comparabil.

macro_rules! maxim {
    ($x:expr) => { $x };
    ($x:expr, $($rest:expr),+) => {{
        let a = $x;
        let b = maxim!($($rest),+);
        if a > b { a } else { b }
    }};
}

fn main() {
    println!("{}", maxim!(4));
    println!("{}", maxim!(3, 9, 7, 5));
    println!("{}", maxim!(2.5, 1.0));
    println!("{}", maxim!("Arad", "Iași", "Bacău"));
}

Rezultat

4
9
2.5
Iași

Exemplul 2: Macro care construiește o colecție

dictionar! primește perechi cheie => valoare și generează câte un insert pentru fiecare, la fel cum vec! generează un push pentru fiecare element. $(,)? acceptă și virgula de la final.

use std::collections::BTreeMap;

macro_rules! dictionar {
    ($($cheie:expr => $valoare:expr),* $(,)?) => {{
        let mut harta = BTreeMap::new();
        $( harta.insert($cheie, $valoare); )*
        harta
    }};
}

fn main() {
    let prefixe = dictionar! {
        "Iași" => "0232",
        "Cluj" => "0264",
        "București" => "021",
    };
    println!("{prefixe:?}");
    println!("Prefix Cluj: {}", prefixe["Cluj"]);
}

Rezultat

{"București": "021", "Cluj": "0264", "Iași": "0232"}
Prefix Cluj: 0264

Exemplul 3: Macro care generează funcții

$nume:ident capturează un nume, folosit apoi ca nume de funcție. Un singur apel al macro-ului creează trei funcții de conversie.

macro_rules! conversie {
    ($($nume:ident: $factor:expr),*) => {
        $(
            fn $nume(valoare: f64) -> f64 {
                valoare * $factor
            }
        )*
    };
}

conversie!(km_in_mile: 0.621371, kg_in_livre: 2.20462, lei_in_euro: 1.0 / 4.97);

fn main() {
    println!("100 km = {:.1} mile", km_in_mile(100.0));
    println!("70 kg = {:.1} livre", kg_in_livre(70.0));
    println!("500 lei = {:.2} euro", lei_in_euro(500.0));
}

Rezultat

100 km = 62.1 mile
70 kg = 154.3 livre
500 lei = 100.60 euro

Greșeli frecvente

  • Folosești macro-ul înainte de definiție în același fișier: cannot find macro `...` in this scope. Mută macro_rules! mai sus.
  • Uiți acoladele duble când macro-ul trebuie să producă o expresie cu mai multe instrucțiuni. Primele acolade aparțin sintaxei ramurii; al doilea rând creează blocul care devine valoarea.
  • Folosești $x de mai multe ori în codul generat: expresia este evaluată de fiecare dată (de exemplu maxim!(citeste(), 5) ar citi de două ori). Salvează-o întâi într-o variabilă (let a = $x;), ca în exemplul 1.

Comenzi înrudite

  • vec!: Creează un Vec cu elementele date, sau cu n copii ale aceleiași valori, într-o singură expresie.
  • println! și print!: Afișează text pe ieșirea standard: println! adaugă la final un rând nou, print! nu. Acoladele {} sunt înlocuite cu valori.
  • atribute (#[derive], #[cfg], #[allow]): Informații pentru compilator scrise cu #[…] deasupra unui element: generează cod (derive), aleg codul compilat (cfg) sau schimbă avertismentele.
  • assert_eq! și assert_ne!: Verifică dacă două valori sunt egale (assert_eq!) sau diferite (assert_ne!); la eșec, panica afișează ambele valori.