Aller au contenu

Les closures en Rust

Une closure (fonction anonyme) est une petite fonction sans nom que vous pouvez ranger dans une variable et faire circuler dans votre programme. Ce qui la distingue d’une fonction classique, c’est qu’elle peut « capturer » des variables de la portée où elle a été créée.

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

Sortie :

6
▶ Essayez par vous-même

Les barres verticales |x: i32| remplacent les parenthèses qu’utiliserait une fonction classique. Comparez les deux côte à côte :

fn add_one_fn(x: i32) -> i32 { x + 1 } // une fonction classique
let add_one_closure = |x: i32| x + 1; // la même chose sous forme de closure

Rust peut souvent deviner aussi le type des paramètres et du retour, donc vous verrez souvent des closures encore plus courtes :

let add_one = |x| x + 1;

Une closure sans paramètre utilise des barres vides : let say_hi = || println!("salut");, et on l’appelle avec say_hi().

Pour plus d’une expression, entourez le corps d’accolades, exactement comme pour une fonction :

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "pair" } else { "impair" };
format!("{n} est {kind}")
};
println!("{}", describe(7));
}

Sortie :

7 est impair
▶ Essayez par vous-même

format! fonctionne comme println!, mais au lieu d’afficher le texte, il le renvoie sous forme de String. Ici, cette String est la dernière expression entre les accolades : c’est donc ce que renvoie la closure.

Voici ce qu’une fonction classique ne sait pas faire : une closure peut utiliser les variables de l’endroit où elle a été définie, sans que vous ayez à les lui passer explicitement.

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

Sortie :

90
40
▶ Essayez par vous-même

discount n’est pas un paramètre de la closure : elle se trouve simplement dans la portée de main, et la closure va la chercher pour l’utiliser directement. Une fn classique ne peut pas faire ça ; il faudrait lui passer discount en argument.

Les closures qui modifient les variables capturées

Section intitulée « Les closures qui modifient les variables capturées »

Une closure peut aussi modifier une variable qu’elle capture, à condition que cette variable soit mut. Il y a une règle de plus : la closure elle-même doit aussi être rangée dans une variable mut. L’appeler modifie quelque chose, et Rust veut que cela se voie dans votre code.

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("le compteur vaut maintenant {count}");
};
increment();
increment();
increment();
println!("valeur finale : {count}");
}

Sortie :

le compteur vaut maintenant 1
le compteur vaut maintenant 2
le compteur vaut maintenant 3
valeur finale : 3
▶ Essayez par vous-même

Si vous oubliez le mut sur increment, le compilateur vous dit exactement ce qui ne va pas : calling increment requires mutable binding due to mutable borrow of count. Autrement dit, appeler increment exige une variable mutable, parce que la closure emprunte count en mode mutable.

Il y a un piège à connaître. Tant que la closure peut encore être appelée, elle a le droit de modifier count, donc personne d’autre n’a le droit d’y toucher. Si vous ajoutez println!("{count}"); entre deux appels à increment(), vous obtenez error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable : impossible de lire count pendant qu’elle est empruntée pour être modifiée. Après le dernier appel à increment(), la closure a fini son travail et count est de nouveau à vous. C’est pour ça que la ligne valeur finale fonctionne. Vous découvrirez toute l’histoire derrière cette règle à la page Borrowing.

Parfois, une closure doit prendre l’ownership (la possession) complète de ce qu’elle capture, par exemple quand on la confie à un autre thread. On écrit alors move avant les barres : move || { ... }. Vous le verrez en action à la page Concurrence.

Vous utiliserez rarement une closure toute seule. Là où elles sont vraiment utiles, c’est pour passer un comportement personnalisé à une autre fonction, en particulier aux méthodes d’itérateurs que vous découvrirez à la page Itérateurs :

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

Sortie :

[2, 4, 6, 8, 10]
▶ Essayez par vous-même

Ici, |n| n * 2 est une closure passée directement à .map(). C’est ce schéma (une petite closure qui pilote une méthode comme .map() ou .filter()) que vous utiliserez vraiment au quotidien.

Créez une variable tax_percent qui vaut 20. Puis écrivez une closure with_tax qui prend un prix et renvoie ce prix taxes comprises, en utilisant la variable capturée tax_percent. Affichez le résultat pour les prix 50 et 120. Indice : la taxe vaut price * tax_percent / 100.

Voir la solution
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 avec les taxes donne {}", with_tax(50));
println!("120 avec les taxes donne {}", with_tax(120));
}

Sortie :

50 avec les taxes donne 60
120 avec les taxes donne 144
▶ Essayez par vous-même

On ne passe jamais tax_percent en paramètre. La closure la capture depuis main, ce qu’une fn classique ne pourrait justement pas faire.

Écrivez une closure next_id qui renvoie 1 la première fois que vous l’appelez, 2 la deuxième fois, 3 la troisième, et ainsi de suite. Appelez-la trois fois et affichez chaque identifiant. Indice : gardez un compteur mut en dehors de la closure, ajoutez-lui 1 à l’intérieur, et faites du compteur la dernière expression de la closure pour qu’il soit renvoyé.

Voir la solution
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("le premier utilisateur reçoit l'id {}", next_id());
println!("le deuxième utilisateur reçoit l'id {}", next_id());
println!("le troisième utilisateur reçoit l'id {}", next_id());
}

Sortie :

le premier utilisateur reçoit l'id 1
le deuxième utilisateur reçoit l'id 2
le troisième utilisateur reçoit l'id 3
▶ Essayez par vous-même

last_id et next_id ont tous les deux besoin de mut : le premier parce que sa valeur change, le second parce qu’appeler la closure modifie ce qu’elle a capturé.

  • Une closure est une petite fonction anonyme : |paramètres| expression.
  • Les types sont souvent devinés par Rust, donc une closure est généralement plus courte que la fn équivalente.
  • Contrairement à une fonction classique, une closure peut capturer des variables de la portée où elle a été définie.
  • Une closure qui modifie une variable capturée doit être rangée dans une variable mut, et elle emprunte cette variable tant qu’on l’utilise.
  • Les closures sont surtout utiles quand on les passe à d’autres fonctions, en particulier aux méthodes d’itérateurs comme .map().

Petit quiz

1. Quels symboles une closure utilise-t-elle à la place des parenthèses d'une fonction pour ses paramètres ?

2. Que peut faire une closure qu'une fn classique ne peut pas faire ?

3. D'après cette page, où utilise-t-on le plus souvent les closures en pratique ?

4. Une closure fait count += 1 sur une variable capturée. De quoi d'autre avez-vous besoin ?

Score : 0 / 4