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 qui42due e 2Nessuna 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.
Scrivere una macro semplice
Sezione intitolata “Scrivere una macro semplice”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($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.
Una macro che accetta più argomenti
Sezione intitolata “Una macro che accetta più argomenti”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:
630$($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.
Più di una regola
Sezione intitolata “Più di una regola”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:
792.5Per 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 insum!) significa zero o più. La regola 2 ha bisogno di almeno un valore dopo la virgola, così la chiamata internamax!(...)ha sempre qualcosa su cui lavorare.max!funziona sia con gli interi sia con i float, perché la macro si limita a incollare il codicefirst > 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" |
Dove incontrerai davvero le macro
Sezione intitolata “Dove incontrerai davvero le macro”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.
Pratica
Sezione intitolata “Pratica”1. Stampa tutto
Sezione intitolata “1. Stampa tutto”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:
mela423.5La 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.
2. Il tuo vec!
Sezione intitolata “2. Il tuo vec!”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]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à.
Riepilogo
Sezione intitolata “Riepilogo”- 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
Sezione intitolata “Verifica veloce”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
Per approfondire
Sezione intitolata “Per approfondire”- Macros — The Rust Book (in inglese): come è definito
vec!, più un’introduzione alle macro procedurali come#[derive]. macro_rules!— Rust by Example (in inglese): esempi brevi ed eseguibili su designatori, overloading e ripetizione.- Macros By Example — The Rust Reference (in inglese): l’elenco completo dei tipi di frammento come
expr,identett, e come funziona esattamente il riconoscimento. - The Little Book of Rust Macros (in inglese): un approfondimento gratuito e alla mano per quando vorrai scrivere macro più grandi.