Aller au contenu

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 ici
42
deux et 2
▶ Essayez par vous-même

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

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
▶ Essayez par vous-même

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

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 :

6
30
▶ Essayez par vous-même

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

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 :

7
9
2.5
▶ Essayez par vous-même

Pour 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 dans sum!) signifie zéro fois ou plus. La règle 2 exige au moins une valeur après la virgule, donc l’appel intérieur max!(...) a toujours quelque chose à traiter.
  • max! fonctionne avec des entiers comme avec des flottants, car la macro se contente de coller le code first > 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"

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.

É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 :

pomme
42
3.5
▶ Essayez par vous-même

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

É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]
▶ Essayez par vous-même

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.

  • 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

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