Les macros en Rust
Sur la page La syntaxe de Rust, vous avez appris une règle simple : un ! signifie qu’il s’agit d’une macro, et non d’une fonction ordinaire : println!, vec!, format!. Cette page explique enfin ce que cela veut vraiment dire.
Pourquoi println! ne peut pas être une fonction ordinaire
Section intitulée « Pourquoi println! ne peut pas être une fonction ordinaire »Une fonction ordinaire comme fn add(a: i32, b: i32) -> i32 prend toujours un nombre fixe d’arguments, de types fixes. Pourtant, vous pouvez appeler println! avec des nombres et des types d’arguments complètement différents :
fn main() { println!("aucun argument ici"); println!("{}", 42); println!("{} et {}", "deux", 2);}Sortie :
aucun argument ici42deux et 2Aucune signature de fonction ordinaire ne pourrait accepter tous ces appels. Les macros fonctionnent autrement : elles s’exécutent avant que votre code soit entièrement compilé, et génèrent du vrai code Rust selon la façon dont elles ont été appelées. println!("{} et {}", "deux", 2) se transforme en fait en du vrai code (on parle d’« expansion » de la macro) qui gère exactement ces deux arguments, avec une expansion différente pour chaque appel différent.
Écrire une macro simple
Section intitulée « Écrire une macro simple »Vous pouvez définir vos propres macros avec macro_rules!. En voici une qui calcule le carré d’un nombre :
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { println!("{}", square!(5));}Sortie :
25($x:expr) veut dire « accepte une expression, et appelle-la $x ». La partie => { ... } est le code que produit la macro : partout où vous écrivez square!(5), le compilateur le remplace par 5 * 5 avant de compiler quoi que ce soit.
Une macro qui accepte plusieurs arguments
Section intitulée « Une macro qui accepte plusieurs arguments »Les macros peuvent reconnaître des motifs plus complexes, y compris une liste d’arguments qui se répète. C’est quelque chose qu’une fonction ordinaire ne sait tout simplement pas faire :
macro_rules! sum { ($($x:expr),*) => { { let mut total = 0; $(total += $x;)* total } };}
fn main() { println!("{}", sum!(1, 2, 3)); println!("{}", sum!(10, 20));}Sortie :
630$($x:expr),* signifie « zéro expression ou plus, séparées par des virgules ». C’est ce même motif qui permet à vec![1, 2, 3] et à vec![1, 2, 3, 4, 5] de fonctionner avec une seule définition de macro.
Plusieurs règles
Section intitulée « Plusieurs règles »Une macro peut avoir plusieurs règles, séparées par des points-virgules. Rust les essaie de haut en bas et utilise la première qui correspond. Une règle peut même rappeler la macro. Voici un max! qui fonctionne avec n’importe quel nombre d’arguments :
macro_rules! max { // Règle 1 : une seule valeur. La plus grande, c'est elle. ($x:expr) => { $x }; // Règle 2 : une valeur, puis une ou plusieurs autres. ($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));}Sortie :
792.5Pour max!(3, 9, 4), la règle 1 ne correspond pas (il y a plus d’une valeur), c’est donc la règle 2 qui s’applique. Elle met 3 de côté et produit max!(9, 4), qui produit à son tour max!(4). Celui-là correspond à la règle 1, et tout s’arrête. L’ordre compte : Rust essaie toujours la règle 1 en premier.
Deux petites nouveautés de syntaxe ici :
+signifie une fois ou plus, alors que*(vu danssum!) signifie zéro fois ou plus. La règle 2 exige au moins une valeur après la virgule, donc l’appel intérieurmax!(...)a toujours quelque chose à traiter.max!fonctionne avec des entiers comme avec des flottants, car la macro se contente de coller le codefirst > biggest_of_rest. Une fonction aurait besoin des génériques pour faire la même chose.
expr n’est pas le seul genre d’élément qu’une macro peut accepter. En voici quelques autres que vous verrez dans de vraies macros :
| Fragment | Correspond à | Exemple |
|---|---|---|
expr |
n’importe quelle expression | 2 + 3, name.len() |
ident |
un nom | count, Point |
ty |
un type | i32, Vec<String> |
literal |
une valeur littérale | 42, "salut" |
Où vous rencontrerez vraiment des macros
Section intitulée « Où vous rencontrerez vraiment des macros »Vous utiliserez des macros en permanence (println!, vec!, format!, #[derive(Debug)]) sans jamais avoir besoin d’écrire les vôtres. Écrire des macros personnalisées est une compétence de niche, surtout utilisée par les auteurs de bibliothèques qui doivent éliminer du code répétitif (boilerplate) dans une grosse base de code. Il est bon de savoir qu’elles existent et comment elles fonctionnent dans les grandes lignes, mais ce n’est pas un outil à sortir dès le début.
Exercices
Section intitulée « Exercices »1. Tout afficher
Section intitulée « 1. Tout afficher »Écrivez une macro print_all! qui prend n’importe quel nombre de valeurs, séparées par des virgules, et affiche chacune sur sa propre ligne. print_all!("pomme", 42, 3.5) doit afficher trois lignes. Indice : reconnaissez $($x:expr),* comme le fait sum!, et répétez un println! pour chaque valeur avec $( ... )*.
Voir la solution
macro_rules! print_all { ($($x:expr),*) => { $(println!("{}", $x);)* };}
fn main() { print_all!("pomme", 42, 3.5);}Sortie :
pomme423.5La macro se transforme en trois appels println! distincts, un par argument. Chaque valeur peut avoir un type différent, ce qu’une fonction normale ne saurait pas gérer.
2. Votre propre vec!
Section intitulée « 2. Votre propre vec! »Écrivez my_vec!, qui fonctionne comme vec!. my_vec![] donne un Vec vide, et my_vec![1, 2, 3] donne un Vec contenant ces éléments. Indice : utilisez deux règles. La première ne reconnaît rien du tout : (). La seconde reconnaît $($x:expr),+, crée un Vec::new() et y ajoute chaque $x avec push.
Voir la solution
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);}Sortie :
[] [1, 2, 3]Vous pouvez appeler n’importe quelle macro avec (), [] ou {}. On utilise les crochets pour les macros qui ressemblent à des listes, comme vec!, mais my_vec!(1, 2, 3) fonctionne aussi. Le Vec vide a besoin du type : Vec<i32>, car sans aucun élément, Rust ne peut pas deviner ce qu’il contiendra.
Récapitulatif
Section intitulée « Récapitulatif »- Une macro se transforme en vrai code avant la compilation : c’est ainsi que
println!peut accepter un nombre et des types d’arguments différents à chaque appel. - Un
!après un nom est toujours le signe que vous appelez une macro, et non une fonction. macro_rules!vous permet de définir les vôtres, en reconnaissant des motifs comme une seule expression ($x:expr) ou une liste répétée ($($x:expr),*).- Une macro peut avoir plusieurs règles. Rust les essaie de haut en bas, et une règle peut rappeler la macro.
- Écrire des macros est une compétence de niche : celles qui existent déjà dans la bibliothèque standard couvrent presque tous vos besoins.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Pourquoi println! ne peut-elle pas être écrite comme une fonction ordinaire ?
2. Que signifie $x:expr dans un motif de macro_rules! ?
3. Quand les macros se transforment-elles en vrai code ?
Score : 0 / 3
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Macros — The Rust Book (en anglais) : comment
vec!est défini, plus une introduction aux macros procédurales comme#[derive]. macro_rules!— Rust by Example (en anglais) : de courts exemples exécutables sur les désignateurs, la surcharge et la répétition.- Macros By Example — The Rust Reference (en anglais) : la liste complète des types de fragments comme
expr,identettt, et le fonctionnement exact de la correspondance. - The Little Book of Rust Macros (en anglais) : une exploration gratuite et accessible, pour le jour où vous voudrez écrire de plus grosses macros.