Salta ai contenuti

Macro in Rust

Nella pagina sulla Sintassi hai imparato la regola pratica: un ! significa che si tratta di una macro, non di una normale funzione, come println!, vec! e format!. Questa pagina spiega finalmente cosa vuol dire davvero.

Perché println! non può essere una normale funzione

Sezione intitolata “Perché println! non può essere una normale funzione”

Una normale funzione come fn add(a: i32, b: i32) -> i32 accetta sempre un numero fisso di argomenti, di tipi fissi. Invece puoi chiamare println! con un numero e dei tipi di argomenti diversissimi:

fn main() {
println!("nessun argomento qui");
println!("{}", 42);
println!("{} e {}", "due", 2);
}

Output:

nessun argomento qui
42
due e 2
▶ Provalo tu

Nessuna firma di funzione ordinaria potrebbe accettare tutte queste chiamate. Le macro funzionano in un altro modo: vengono eseguite prima che il codice sia compilato del tutto, e generano vero codice Rust in base a come sono state chiamate. println!("{} e {}", "due", 2) in pratica si espande in codice reale che gestisce esattamente quei due argomenti, con un’espansione diversa per ogni chiamata diversa.

Puoi definire le tue macro con macro_rules!. Eccone una che eleva un numero al quadrato:

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

Output:

25
▶ Provalo tu

($x:expr) significa «accetta un’espressione e chiamala $x». La parte => { ... } è il codice in cui si espande la macro: ovunque scrivi square!(5), il compilatore sostituisce 5 * 5 prima ancora di compilare qualcosa.

Le macro possono riconoscere pattern più complessi, compresa una lista di argomenti che si ripete. È una cosa che una normale funzione proprio non può fare:

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
▶ Provalo tu

$($x:expr),* significa «zero o più espressioni, separate da virgole». È lo stesso pattern che permette sia a vec![1, 2, 3] sia a vec![1, 2, 3, 4, 5] di funzionare con un’unica definizione di macro.

Una macro può avere diverse regole, separate da punti e virgola. Rust le prova dall’alto verso il basso e usa la prima che corrisponde. Una regola può persino richiamare la macro stessa. Ecco un max! che funziona con qualsiasi numero di argomenti:

macro_rules! max {
// Regola 1: un solo valore. Il più grande è il valore stesso.
($x:expr) => {
$x
};
// Regola 2: un valore, poi uno o più altri.
($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
▶ Provalo tu

Per max!(3, 9, 4) la regola 1 non corrisponde (c’è più di un valore), quindi si usa la regola 2. Questa mette da parte il 3 ed espande max!(9, 4), che a sua volta espande max!(4). Quest’ultima corrisponde alla regola 1 e si ferma lì. L’ordine conta: Rust prova sempre prima la regola 1.

Qui ci sono due piccoli pezzi di sintassi nuovi:

  • + significa uno o più, mentre * (visto in sum!) significa zero o più. La regola 2 ha bisogno di almeno un valore dopo la virgola, così la chiamata interna max!(...) ha sempre qualcosa su cui lavorare.
  • max! funziona sia con gli interi sia con i float, perché la macro si limita a incollare il codice first > biggest_of_rest. Una funzione avrebbe bisogno dei generici per farlo.

expr non è l’unico tipo di cosa che una macro può accettare. Eccone alcuni altri che vedrai nelle macro reali:

Frammento Corrisponde a Esempio
expr qualsiasi espressione 2 + 3, name.len()
ident un nome count, Point
ty un tipo i32, Vec<String>
literal un valore letterale 42, "hi"

Userai le macro di continuo (println!, vec!, format!, #[derive(Debug)]) senza mai aver bisogno di scriverne una tua. Scrivere macro personalizzate è una competenza di nicchia, usata soprattutto da chi scrive librerie e deve eliminare codice ripetitivo vero e proprio in una codebase grande. È utile sapere che esistono e, a grandi linee, come funzionano, ma non sono qualcosa da usare fin dall’inizio.

Scrivi una macro print_all! che accetta un numero qualsiasi di valori, separati da virgole, e stampa ognuno su una riga sua. print_all!("mela", 42, 3.5) deve stampare tre righe. Suggerimento: riconosci $($x:expr),* come fa sum! e ripeti un println! per ciascuno con $( ... )*.

Mostra la soluzione
macro_rules! print_all {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
fn main() {
print_all!("mela", 42, 3.5);
}

Output:

mela
42
3.5
▶ Provalo tu

La macro si espande in tre chiamate println! separate, una per argomento. Ogni valore può avere un tipo diverso, cosa che una normale funzione non potrebbe gestire.

Scrivi my_vec!, che funziona come vec!. my_vec![] dà un Vec vuoto, e my_vec![1, 2, 3] dà un Vec con quegli elementi. Suggerimento: usa due regole. La prima non riconosce proprio niente, (). La seconda riconosce $($x:expr),+, crea un Vec::new() e ci inserisce ogni $x con push.

Mostra la soluzione
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]
▶ Provalo tu

Puoi chiamare qualsiasi macro con (), [] o {}. Per le macro che creano liste, come vec!, si usano di solito le parentesi quadre, ma anche my_vec!(1, 2, 3) funziona. Il Vec vuoto ha bisogno del tipo : Vec<i32>, perché senza elementi Rust non può capire cosa conterrà.

  • Una macro si espande in codice reale prima della compilazione: è così che println! può accettare un numero e dei tipi di argomenti diversi a ogni chiamata.
  • Il ! dopo un nome è sempre il segno che stai chiamando una macro, non una funzione.
  • macro_rules! ti permette di definire le tue, riconoscendo pattern come una singola espressione ($x:expr) o una lista ripetuta ($($x:expr),*).
  • Una macro può avere più regole. Rust le prova dall’alto verso il basso, e una regola può richiamare la macro.
  • Scrivere macro è una competenza di nicchia: usare quelle già presenti nella libreria standard copre quasi tutto ciò che ti servirà.

Verifica veloce

1. Perché println! non si può scrivere come una normale funzione?

2. Cosa significa $x:expr dentro un pattern di macro_rules!?

3. Quando si espandono le macro in codice reale?

Punteggio: 0 / 3