Fn, FnMut et FnOnce en Rust
Sur la page Les closures, vous avez appris à écrire une closure (fonction anonyme), à lui faire capturer des variables et même à les modifier. Cette page répond à la question suivante : comment écrire votre propre fonction qui accepte une closure ? Et comment renvoyer une closure depuis une fonction ?
La réponse tient en trois petits traits : Fn, FnMut et FnOnce. Imaginez le règlement d’une bibliothèque. Certains visiteurs se contentent de lire les livres (Fn). D’autres griffonnent des notes dans les marges (FnMut). Et d’autres emportent un livre chez eux sans jamais le rapporter (FnOnce). Rust regarde ce que fait votre closure et lui colle l’étiquette qui correspond. Si ces traits vous paraissent intimidants au début, pas d’inquiétude : il n’y en a que trois, et c’est le compilateur qui choisit le bon pour vous.
Pourquoi les closures ont-elles besoin de traits ?
Section intitulée « Pourquoi les closures ont-elles besoin de traits ? »Chaque closure a son propre type, unique, inventé par le compilateur. Ce type n’a pas de nom que vous puissiez écrire. Même deux closures parfaitement identiques ont des types différents :
fn main() { let mut rules = vec![|price: u32| price * 2]; rules.push(|price: u32| price * 2);}error[E0308]: mismatched types --> src/main.rs:3:16 |3 | rules.push(|price: u32| price * 2); | ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure | = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objectLe compilateur explique (en anglais) qu’il attendait une closure et en a trouvé une autre : deux closures n’ont jamais le même type, même si elles sont identiques.
Une fonction ne peut donc pas dire « donnez-moi une closure de type X ». Elle dit plutôt « donnez-moi n’importe quel type qu’on peut appeler de telle façon ». C’est une contrainte de trait (trait bound), exactement comme celles des pages Les génériques et Les traits :
fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 { rule(price)}
fn main() { let coupon = 15; let half_price = |price: u32| price / 2; let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80)); println!("{}", price_after(use_coupon, 80));}Sortie :
4065Lisez F: Fn(u32) -> u32 ainsi : « F est un type qu’on peut appeler avec un u32, et qui renvoie un u32 ». Les deux closures conviennent, même si leurs types sont différents.
Les trois traits : Fn, FnMut et FnOnce
Section intitulée « Les trois traits : Fn, FnMut et FnOnce »Le compilateur regarde ce que le corps de la closure fait des valeurs qu’elle a capturées :
Fn: elle se contente de les lire. Vous pouvez l’appeler autant de fois que vous voulez.FnMut: elle les modifie. Vous pouvez l’appeler plusieurs fois, mais il fautmut.FnOnce: elle les cède (elle les déplace hors d’elle-même). Vous ne pouvez l’appeler qu’une seule fois, car ensuite la valeur n’est plus là.
fn main() { let shop = String::from("Boulangerie du Coin");
// Fn : se contente de lire `shop` let greet = || println!("Bienvenue à la {shop} !"); greet(); greet();
// FnMut : modifie `orders` let mut orders = 0; let mut take_order = || { orders += 1; println!("commande n°{orders} prise"); }; take_order(); take_order();
// FnOnce : rend `receipt` à l'appelant let receipt = String::from("2 croissants, 1 café"); let hand_over = || receipt; let customer_copy = hand_over(); println!("le client a : {customer_copy}");}Sortie :
Bienvenue à la Boulangerie du Coin !Bienvenue à la Boulangerie du Coin !commande n°1 prisecommande n°2 prisele client a : 2 croissants, 1 caféhand_over renvoie receipt lui-même, pas une copie. Une fois le ticket de caisse remis, la closure n’a plus rien à donner. Voilà pourquoi elle est FnOnce.
Voici l’image à garder en tête. Imaginez trois cercles, l’un dans l’autre :
- Le grand cercle extérieur, c’est
FnOnce. Toute closure peut être appelée au moins une fois, donc toutes les closures sont dedans. - À l’intérieur, un cercle plus petit,
FnMut: les closures qu’on peut appeler encore et encore. - Tout au centre,
Fn: les closures qu’on peut appeler encore et encore sans rien modifier.
Toute closure Fn est donc aussi FnMut et FnOnce. C’est important quand vous écrivez une fonction : un paramètre qui demande FnOnce accepte n’importe quelle closure, alors qu’un paramètre qui demande Fn est le plus exigeant.
Une chose surprend souvent : le mot-clé move ne choisit pas le trait. move || println!("{shop}") prend l’ownership (possession) de shop, mais se contente de le lire, donc elle reste Fn. Seul le corps de la closure décide.
Écrire des fonctions qui prennent des closures
Section intitulée « Écrire des fonctions qui prennent des closures »Il existe trois façons d’écrire la même contrainte. Elles veulent toutes dire la même chose : choisissez celle qui se lit le mieux.
// 1. Un paramètre générique avec une contraintefn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x)}
// 2. `impl Fn` en position d'argument : plus court, pas besoin de nomfn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x))}
// 3. Une clause `where` : plus lisible quand la contrainte s'allongefn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>where F: Fn(i32) -> i32,{ scores.iter().map(|&s| f(s)).collect()}
fn main() { let bonus = 10; let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50)); println!("{}", apply_twice(add_bonus, 50)); println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));}Sortie :
6070[50, 75, 100]Vous vous demandez peut-être comment on a pu passer add_bonus trois fois. Une closure qui se contente de lire ce qu’elle capture ne coûte presque rien à copier, alors Rust la copie à chaque fois.
Passons à FnMut et FnOnce. Avec FnMut, le paramètre a besoin de mut, parce qu’appeler la closure modifie ce qu’elle contient. Avec FnOnce, vous l’appelez une fois, et c’est tout :
fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { for task in tasks { on_done(task); }}
fn on_shutdown<F: FnOnce()>(cleanup: F) { println!("arrêt en cours..."); cleanup();}
fn main() { let mut finished = Vec::new(); process_tasks(&["envoyer les e-mails", "sauvegarder la base"], |task| { finished.push(task.to_string()); }); println!("terminées : {:?}", finished);
on_shutdown(|| { // Cette boucle prend l'ownership de `finished`, donc la closure est FnOnce for task in finished { println!("archivée : {task}"); } });}Sortie :
terminées : ["envoyer les e-mails", "sauvegarder la base"]arrêt en cours...archivée : envoyer les e-mailsarchivée : sauvegarder la baseUne bonne règle : demandez le trait le moins exigeant dont votre fonction peut se contenter. Si vous n’appelez la closure qu’une fois, demandez FnOnce, et les appelants pourront passer n’importe quoi. La bibliothèque standard fait pareil : Option::map prend un FnOnce, tandis que Iterator::map prend un FnMut.
Quand vous choisissez la mauvaise contrainte
Section intitulée « Quand vous choisissez la mauvaise contrainte »Le compilateur détecte chaque incohérence, et ses messages sont ici très bienveillants. Imaginons qu’une fonction demande Fn, mais que votre closure modifie un compteur :
fn run_report<F: Fn()>(f: F) { f(); f();}
fn main() { let mut runs = 0; run_report(|| { runs += 1; println!("rapport n°{runs} généré"); });}error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure --> src/main.rs:9:9 |1 | fn run_report<F: Fn()>(f: F) { | - change this to accept `FnMut` instead of `Fn`...8 | run_report(|| { | ---------- -- in this closure | | | expects `Fn` instead of `FnMut`9 | runs += 1; | ^^^^^^^^^ cannot assignLe message dit qu’on ne peut pas modifier runs dans une closure Fn, et il vous donne même la solution : accepter FnMut à la place de Fn, et ajouter mut au paramètre. Avec ce changement, le programme affiche rapport n°1 généré puis rapport n°2 généré :
fn run_report<F: FnMut()>(mut f: F) { f(); f();}L’autre erreur classique, c’est d’appeler deux fois une closure FnOnce :
fn main() { let report = String::from("ventes du T3"); let send = || { let sent = report; println!("envoi de {sent}"); }; send(); send();}error[E0382]: use of moved value: `send` --> src/main.rs:8:5 |7 | send(); | ------ `send` moved due to this call8 | send(); | ^^^^ value used here after move |note: closure cannot be invoked more than once because it moves the variable `report` out of its environment --> src/main.rs:4:20 |4 | let sent = report; | ^^^^^^La note le dit clairement : la closure ne peut pas être appelée plus d’une fois, car le premier appel a cédé report, et il ne reste plus rien pour un deuxième appel. Si vous avez vraiment besoin de deux appels, empruntez plutôt la valeur (let sent = &report;) : la closure devient alors Fn. Si vous passez une closure de ce genre à une fonction qui demande Fn, vous obtenez un message proche : error[E0507]: cannot move out of report, a captured variable in an Fn closure.
Renvoyer des closures avec impl Fn
Section intitulée « Renvoyer des closures avec impl Fn »Une fonction peut aussi fabriquer une closure et la renvoyer. Vous écrivez le type de retour impl Fn(i32) -> i32, qui veut dire « un certain type de closure, dont vous n’avez pas besoin de connaître le nom » :
fn make_adder(amount: i32) -> impl Fn(i32) -> i32 { move |x| x + amount}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor}
fn main() { let add_tip = make_adder(5); let triple = make_multiplier(3);
println!("{}", add_tip(20)); println!("{}", triple(7)); println!("{}", add_tip(triple(10)));}Sortie :
252135Pourquoi move ? Sans lui, la closure se contenterait d’emprunter amount. Or amount appartient à make_adder, et disparaît dès que make_adder se termine. La closure garderait alors une référence vers rien du tout. Rust refuse :
error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function --> src/main.rs:2:5 |2 | |x| x + amount | ^^^ ------ `amount` is borrowed here | | | may outlive borrowed value `amount` |help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword |2 | move |x| x + amount | ++++Le compilateur explique que la closure risque de vivre plus longtemps que la fonction alors qu’elle emprunte amount, et propose d’ajouter move. Avec move, la closure emporte sa propre copie de amount, un peu comme si vous preniez votre déjeuner avec vous au lieu de le laisser dans le frigo de quelqu’un d’autre.
Les closures dans une Box : plusieurs closures, un seul type
Section intitulée « Les closures dans une Box : plusieurs closures, un seul type »impl Fn désigne un seul type caché. Vous ne pouvez donc pas renvoyer une closure depuis la branche if et une autre depuis la branche else. Vous ne pouvez pas non plus mettre des closures différentes dans le même Vec. Dans les deux cas, rangez chaque closure dans une boîte : Box<dyn Fn(f64) -> f64> veut dire « n’importe quelle closure de cette forme, stockée sur le tas ». C’est la même idée que sur la page Trait objects (dyn).
Voici une caisse qui applique, dans l’ordre, une liste de règles de prix nommées :
fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { if express { Box::new(move |total| total + fee * 2.0) } else { Box::new(move |total| total + fee) }}
fn main() { let member_percent = 10.0; let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![ ("remise membre", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))), ("bon de réduction", Box::new(move |total: f64| total - coupon)), ("livraison standard", shipping_rule(false, 3.5)), ];
let mut total = 80.0; println!("départ : {total:.2}"); for (name, rule) in &rules { total = rule(total); println!("après {name} : {total:.2}"); }}Sortie :
départ : 80.00après remise membre : 72.00après bon de réduction : 67.00après livraison standard : 70.50Mettre une closure dans une Box coûte une toute petite allocation sur le tas, et chaque appel passe par un pointeur. Ce n’est presque jamais un problème. Utilisez impl Fn quand il n’y a qu’un type de closure, et Box<dyn Fn> quand il y en a plusieurs.
Ranger une closure dans une struct
Section intitulée « Ranger une closure dans une struct »Une struct (structure) peut aussi contenir une closure. Comme vous ne pouvez pas nommer le type de la closure, vous rendez la struct générique sur ce type. Voici un petit bouton qui exécute du code quand on clique dessus :
struct Button<F: Fn()> { label: String, on_click: F,}
impl<F: Fn()> Button<F> { fn click(&self) { println!("[{}] cliqué", self.label); (self.on_click)(); }}
fn main() { let user = String::from("Léa"); let save = Button { label: String::from("Enregistrer"), on_click: move || println!("enregistrement du brouillon de {user}"), };
save.click(); save.click();}Sortie :
[Enregistrer] cliquéenregistrement du brouillon de Léa[Enregistrer] cliquéenregistrement du brouillon de LéaRemarquez les parenthèses dans (self.on_click)(). Elles disent à Rust : « prends le champ, puis appelle-le ». Si le bouton doit modifier ce qu’il capture à chaque clic, utilisez F: FnMut() et faites prendre &mut self à la méthode.
Les pointeurs de fonction : fn(i32) -> i32
Section intitulée « Les pointeurs de fonction : fn(i32) -> i32 »Les fonctions classiques peuvent aussi circuler dans votre programme. Leur type s’écrit avec un petit fn, comme fn(i32) -> i32. C’est un pointeur de fonction, c’est-à-dire simplement l’adresse d’un morceau de code. Une closure qui ne capture rien peut, elle aussi, devenir un pointeur de fonction :
fn double(x: i32) -> i32 { x * 2}
fn square(x: i32) -> i32 { x * x}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 { op(x)}
fn main() { println!("{}", apply_op(double, 4)); println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1]; let mut value = 3; for step in steps { value = step(value); } println!("{value}");}Sortie :
8337Tous les pointeurs de fonction partagent le même type, donc vous pouvez les ranger dans un tableau sans Box. Le revers de la médaille : une closure qui capture quelque chose ne peut pas devenir un pointeur de fonction. Le compilateur vous dit alors closures can only be coerced to fn types if they do not capture any variables (seules les closures qui ne capturent aucune variable peuvent devenir des types fn). La bonne nouvelle va dans l’autre sens : toute fonction classique implémente aussi Fn, FnMut et FnOnce, donc vous pouvez passer double à la fonction apply vue plus haut. C’est pour ça que la plupart du code utilise les traits de closure : ils acceptent les deux.
Erreurs courantes
Section intitulée « Erreurs courantes »Oublier mut sur un paramètre FnMut. Appeler un FnMut le modifie, donc la variable qui le contient doit être mut :
error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable --> src/main.rs:3:9 |3 | on_done(task); | ^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { | +++Le compilateur indique où ajouter mut (les +++ sous la ligne 1).
Renvoyer des closures différentes depuis -> impl Fn. Deux closures, ce sont deux types, même si elles se ressemblent :
fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 { if express { move |total| total + fee * 2.0 } else { move |total| total + fee }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if express {3 | | move |total| total + fee * 2.0 | | ------------------------------ | | | | | the expected closure | | expected because of this4 | | } else {5 | | move |total| total + fee | | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure6 | | } | |_____- `if` and `else` have incompatible types | = note: expected closure `{closure@src/main.rs:3:9: 3:21}` found closure `{closure@src/main.rs:5:9: 5:21}` = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objecthelp: you could change the return type to be a boxed trait object |1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new(move |total| total + fee * 2.0)4 | } else {5 ~ Box::new(move |total| total + fee) |Faites ce que propose le compilateur : renvoyez Box<dyn Fn(f64) -> f64> et enveloppez chaque closure dans Box::new(...).
Appeler un champ closure comme une méthode. Si vous écrivez self.on_click(), Rust cherche une méthode nommée on_click :
error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope --> src/main.rs:9:14 |9 | self.on_click(); | ^^^^^^^^ field, not a method |help: to call the type parameter stored in `on_click`, surround the field access with parentheses |9 | (self.on_click)(); | + +Le message rappelle que on_click est un champ, pas une méthode, et propose d’entourer l’accès au champ de parenthèses.
Exercices
Section intitulée « Exercices »1. Un distributeur de tickets
Section intitulée « 1. Un distributeur de tickets »Écrivez une fonction ticket_dispenser(start: u32) qui renvoie une closure. Chaque appel à la closure donne le numéro de ticket suivant : start, puis start + 1, et ainsi de suite. Créez deux distributeurs (une boulangerie qui commence à 100, une pharmacie qui commence à 1) et montrez qu’ils comptent chacun de leur côté. Indice : la closure modifie son propre compteur, donc le type de retour est impl FnMut() -> u32, et vous aurez besoin de move.
Voir la solution
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 { let mut next = start; move || { let ticket = next; next += 1; ticket }}
fn main() { let mut bakery = ticket_dispenser(100); let mut pharmacy = ticket_dispenser(1);
println!("boulangerie : {}", bakery()); println!("boulangerie : {}", bakery()); println!("pharmacie : {}", pharmacy()); println!("boulangerie : {}", bakery());}Sortie :
boulangerie : 100boulangerie : 101pharmacie : 1boulangerie : 102Chaque distributeur a déplacé son propre next à l’intérieur de sa closure, donc ils ne partagent jamais de compteur. Les variables bakery et pharmacy ont besoin de mut parce qu’elles sont FnMut.
2. Une fonction pour réessayer
Section intitulée « 2. Une fonction pour réessayer »Écrivez retry(max_attempts, task). Elle appelle task jusqu’à ce qu’elle renvoie Ok, ou jusqu’à avoir essayé max_attempts fois. Affichez une ligne pour chaque tentative ratée. Testez-la avec une closure qui échoue deux fois avec "serveur occupé", puis réussit. Indice : la closure de test compte ses propres appels, donc la contrainte est FnMut() -> Result<String, String>, et le paramètre a besoin de mut.
Voir la solution
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>where F: FnMut() -> Result<String, String>,{ for attempt in 1..=max_attempts { match task() { Ok(value) => return Ok(value), Err(e) => println!("tentative {attempt} ratée : {e}"), } } Err(String::from("abandon"))}
fn main() { let mut calls = 0; let result = retry(5, || { calls += 1; if calls < 3 { Err(String::from("serveur occupé")) } else { Ok(String::from("données téléchargées")) } }); println!("{:?} après {calls} appels", result);}Sortie :
tentative 1 ratée : serveur occupétentative 2 ratée : serveur occupéOk("données téléchargées") après 3 appelsretry appelle la tâche plusieurs fois, donc FnOnce ne convient pas. Et la tâche modifie calls, donc Fn ne convient pas non plus. FnMut est exactement ce qu’il faut. Quand retry se termine, on n’a plus besoin de la closure, et main peut de nouveau lire calls.
Récapitulatif
Section intitulée « Récapitulatif »- Chaque closure a son propre type unique : les fonctions acceptent donc les closures via une contrainte de trait comme
F: Fn(i32) -> i32. Fnse contente de lire ce qu’elle capture,FnMutle modifie, etFnOncele cède et ne peut être appelée qu’une fois. C’est le corps qui décide, pasmove.- Toute closure
Fnest aussiFnMutetFnOnce. Demandez le trait le moins exigeant dont votre fonction a besoin. - Écrivez les contraintes sous la forme
<F: Fn(..)>,impl Fn(..)ou dans une clausewhere. Les paramètresFnMutont besoin demut. - Renvoyez une closure avec
-> impl Fn(..)(en général avecmove). UtilisezBox<dyn Fn(..)>pour des closures différentes selon les branches ou dans unVec. fn(i32) -> i32est un pointeur de fonction. Les fonctions classiques et les closures qui ne capturent rien y correspondent.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Une closure fait total += price sur une variable capturée. Quel est le trait le plus exigeant qu'elle implémente ?
2. Votre fonction appelle sa closure exactement une fois. Quelle contrainte accepte le plus de closures ?
3. Pourquoi make_adder(amount: i32) -> impl Fn(i32) -> i32 a-t-elle besoin de move |x| x + amount ?
4. Vous voulez un Vec contenant plusieurs closures de remise différentes. Quel type doivent avoir les éléments ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Moving Captured Values Out of Closures and the Fn Traits — The Rust Book (en anglais) : l’explication officielle de la façon dont le compilateur choisit
Fn,FnMutouFnOnce. Fn,FnMutetFnOnce— documentation de la bibliothèque standard (en anglais) : la définition de chaque trait, avec de courts exemples d’utilisation.- Input parameters — Rust by Example (en anglais) : des exemples exécutables de fonctions qui prennent des closures avec chacune des trois contraintes.
- Output parameters — Rust by Example (en anglais) : renvoyer des closures avec
impl Fn, et pourquoimoveest nécessaire. - Closure types — The Rust Reference (en anglais) : les règles exactes sur la façon dont les closures capturent les valeurs et les traits qu’elles implémentent.