Fonctions et fermetures avancées
Dans cette section, nous allons explorer quelques fonctionnalités avancées liées aux fonctions et aux fermetures, y compris les pointeurs de fonctions et la capacité de retourner des fermetures.
Pointeurs de fonctions
Nous avons déjà vu comment envoyer des fermetures dans des fonctions ; mais vous pouvez aussi envoyer des fonctions classiques dans d’autres fonctions ! Cette technique est utile lorsque vous souhaitez envoyer une fonction que vous avez déjà définie plutôt que de définir une nouvelle fermeture. Les fonctions nécessitent le type fn (avec un f minuscule), à ne pas confondre avec le trait de fermeture Fn. Le type fn s’appelle un pointeur de fonction. Le passage de fonctions avec des pointeurs vous permettra d’utiliser des fonctions comme arguments d’autres fonctions.
La syntaxe pour indiquer qu’un paramètre est un pointeur de fonction ressemble à celle des fermetures, comme vous pouvez le voir dans l’encart 20-28, où nous avons défini une fonction ajouter_un qui ajoute 1 à son paramètre. La fonction le_faire_deux_fois prend deux paramètres : un pointeur de fonction vers n’importe quelle fonction qui prend un paramètre i32 et renvoie un i32, et une valeur i32. La fonction le_faire_deux_fois appelle la fonction f deux fois, lui passant la valeur arg, puis additionne les deux résultats des appels de fonction ensemble. La fonction main appelle le_faire_deux_fois avec les arguments ajouter_un et 5.
fn ajouter_un(x: i32) -> i32 {
x + 1
}
fn le_faire_deux_fois(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let reponse = le_faire_deux_fois(ajouter_un, 5);
println!("La réponse est : {reponse}");
}
fn type to accept a function pointer as an argumentCe code affiche La réponse est : 12. Nous avons précisé que le paramètre f dans le_faire_deux_fois est une fn qui prend en argument un paramètre du type i32 et retourne un i32. Nous pouvons ensuite appeler f dans le corps de le_faire_deux_fois. Dans main, nous pouvons envoyer le nom de la fonction ajouter_un dans le premier argument de le_faire_deux_fois.
Contrairement aux fermetures, fn est un type plutôt qu’un trait, donc nous indiquons fn directement comme type de paramètre plutôt que de déclarer un paramètre de type générique avec un des traits Fn comme trait lié.
Les pointeurs de fonctions implémentent simultanément les trois traits de fermeture (Fn, FnMut et FnOnce), ce qui implique que vous pouvez toujours passer un pointeur de fonction en argument d’une fonction qui attend une fermeture. Il vaut mieux écrire des fonctions qui utilisent un type générique et un des traits de fermeture afin que vos fonctions puissent accepter soit des fonctions, soit des fermetures.
Ceci étant dit, une situation dans laquelle vous ne voudrez accepter que des fn et pas des fermetures, est lorsque vous vous interfacez avec du code externe qui n’a pas de fermetures : les fonctions C peuvent accepter des fonctions en argument, mais le C n’a pas fermetures.
Comme exemple d’une situation dans laquelle vous pourriez utiliser soit une fermeture définie directement, soit le nom d’une fonction, considérons l’utilisation de la méthode map fournie par le trait Iterator dans la bibliothèque standard. Pour utiliser la méthode map pour transformer un vecteur de nombres en vecteur de chaînes de caractères, nous pourrions utiliser une fermeture, comme dans l’encart 20-29.
fn main() {
let liste_de_nombres = vec![1, 2, 3];
let liste_de_chaines: Vec<String> =
liste_de_nombres.iter().map(|i| i.to_string()).collect();
}
map method to convert numbers to stringsOu alors nous pourrions utiliser le nom d’une fonction en argument de map plutôt qu’une fermeture, comme le montre l’encart 20-30.
fn main() {
let liste_de_nombres = vec![1, 2, 3];
let liste_de_chaines: Vec<String> =
liste_de_nombres.iter().map(ToString::to_string).collect();
}
String::to_string function with the map method to convert numbers to stringsNotez que nous devons utiliser la syntaxe complète que nous avons vue dans la section “Traits avancés” car il existe plusieurs fonctions disponibles qui s’appellent to_string. Ici, nous utilisons la fonction to_string définie dans le trait ToString que la bibliothèque standard a implémenté sur chaque type qui implémente Display.
Ici, nous utilisons la fonction to_string définie dans le trait ToString, lequel est implémenté par la bibliothèque standard pour tout type qui implémente Display.
Rappelez-vous qu’à la section “Les valeurs d’énumérations” du chapitre 6, nous apprenions que le nom de chaque variante d’énumération que nous déclarons devient aussi une fonction d’initialisation. Nous pouvons utiliser ces fonctions d’initialisation en tant que pointeurs de fonctions qui implémentent les traits de fermetures, ce qui signifie que nous pouvons utiliser les fonctions d’initialisation comme paramètre des méthodes qui acceptent des fermetures, comme dans l’encart 20-31.
fn main() {
enum Statut {
Valeur(u32),
Stop,
}
let liste_de_statuts: Vec<Statut> = (0u32..20).map(Statut::Valeur).collect();
}
map method to create a Status instance from numbersNous avons ici créé des instances de Statut::Valeur en utilisant chacune des valeurs u32 présentes dans l’intervalle sur laquelle nous appelons map en utilisant la fonction d’initialisation de Statut::Valeur. Certaines personnes préfèrent ce style, et d’autres préfèrent utiliser des fermetures. Ces deux approches se compilent et produisent le même code, vous pouvez donc utiliser le style qui est le plus clair pour vous.
Retourner des fermetures
Les fermetures sont représentées par des traits, ce qui signifie que vous ne pouvez pas retourner directement des fermetures. Dans la plupart des situations où vous auriez voulu retourner un trait, vous pouvez utiliser à la place le type concret qui implémente le trait comme valeur de retour de la fonction. Toutefois, vous ne pouvez généralement pas faire ceci avec les fermetures car elles n’ont pas de type concret qu’elles peuvent retourner ; vous n’êtes pas autorisé à utiliser le pointeur de fonction fn comme type de retour si, par exemple, la fermeture capture des valeurs de sa portée.
À la place, vous utiliserez généralement la syntaxe impl Trait que nous avons apprise au chapitre 10. Vous pouvez renvoyer tout type de fonction, en utilisant Fn, FnOnce, et FnMut. Par exemple, le code de l’encart 20-32 se compilera parfaitement.
#![allow(unused)]
fn main() {
fn retourne_une_fermeture() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
}
impl Trait syntaxCependant, comme nous l’avons noté dans la section “Déduction et annotation des types de fermeture” du chapitre 13, chaque fermeture est aussi son propre type spécifique. Si vous avez besoin de travailler avec plusieurs fonctions qui ont la même signature mais des implémentations différentes, vous devrez utiliser un objet trait pour celles-ci. Réfléchissez à ce qui se passe si vous écrivez du code comme montré dans l’encart 20-33.
fn main() {
let gestionnaires = vec![retourne_une_fermeture(), retourne_une_fermeture_initialisee(123)];
for gestionnaire in gestionnaires {
let sortie = gestionnaire(5);
println!("{sortie}");
}
}
fn retourne_une_fermeture() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
fn retourne_une_fermeture_initialisee(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T> of closures defined by functions that return impl Fn typesNous avons ici deux fonctions, retourne_une_fermeture et retourne_une_fermeture_initialisee, qui renvoient toutes deux impl Fn(i32) -> i32. Notez que les fermetures qu’elles renvoient sont différentes, bien qu’elles implémentent le même type. Si nous essayons de compiler cela, Rust nous fait savoir que ça ne va pas fonctionner :
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let gestionnaires = vec![retourne_une_fermeture(), retourne_une_fermeture_initialisee(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn retourne_une_fermeture() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
| ------------------- the found opaque type
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
Le message d’erreur nous indique qu’à chaque fois que nous retournons un impl Trait, Rust crée un type opaque unique, un type dont nous ne pouvons pas voir les détails de la construction interne de Rust, ni deviner le type que Rust va générer pour l’écrire nous-mêmes. Ainsi, même si ces fonctions renvoient des fermetures implémentant le même trait, Fn(i32) -> i32, les types opaques que Rust génère pour chacune d’entre elles sont différents (ceci est similaire à la manière dont Rust produit différents types concrets pour des blocs asynchrones, quand bien même ils ont le même type en sortie, comme nous l’avons vu dans “Le type Pin et le trait Unpin” dans le chapitre 17). Nous déjà vu plusieurs fois une solution à ce problème : nous pouvons utiliser un objet trait, comme dans l’encart 20-34.
fn main() {
let gestionnaires = vec![retourne_une_fermeture(), retourne_une_fermeture_initialisee(123)];
for gestionnaire in gestionnaires {
let sortie = gestionnaire(5);
println!("{sortie}");
}
}
fn retourne_une_fermeture() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn retourne_une_fermeture_initialisee(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
Vec<T> of closures defined by functions that return Box<dyn Fn> so that they have the same typeCe code va se compiler à merveille. Pour en savoir plus sur les objets trait, rendez-vous à la section“Utilisation des objets traits pour abstraire des comportements communs” du chapitre 18.
Maintenant, penchons-nous sur les macros !