Les 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. Vous pouvez faire ceci avec des pointeurs de fonctions, qui vous permettent d'utiliser des fonctions en argument d'autres fonctions. 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. 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 19-27.

Fichier : src/main.rs

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);
}

Encart 19-27 : utiliser le type fn pour accepter un pointeur de fonction en argument

Ce 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) afin que vous puissiez toujours envoyer un pointeur de fonction en argument d'une fonction qui attendait 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.

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 pouvez utiliser soit une fermeture définie directement ou le nom d'une fonction, prenons l'utilisation de map. Pour utiliser la fonction map pour transformer un vecteur de nombres en vecteur de chaînes de caractères, nous pouvons utiliser une fermeture, comme ceci :

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();
}

Ou alors nous pouvons utiliser le nom d'une fonction en argument de map plutôt qu'une fermeture, comme ceci :

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();
}

Notez que nous devons utiliser la syntaxe complète que nous avons vue précédemment dans la section précédente 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.

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

fn main() {
    enum Statut {
        Valeur(u32),
        Stop,
    }

    let liste_de_statuts: Vec<Statut> = (0u32..20).map(Statut::Valeur).collect();
}

Nous 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. Mais vous ne pouvez 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, par exemple.

Le code suivant essaye de retourner directement une fermeture, mais ne peut pas se compiler :

fn retourne_une_fermeture() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}

Voici l'erreur de compilation :

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> src/lib.rs:1:25
  |
1 | fn retourne_une_fermeture() -> dyn Fn(i32) -> i32 {
  |                                ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32`
  |
1 | fn retourne_une_fermeture() -> impl Fn(i32) -> i32 {
  |                                ~~~~~~~~~~~~~~~~~~~

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error

Une nouvelle fois l'erreur du trait Sized ! Rust ne sait pas combien de mémoire sera nécessaire pour stocker la fermeture. Nous avons vu une solution à ce problème précédemment. Nous pouvons utiliser un objet trait :

fn retourne_une_fermeture() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

Ce code va se compiler à merveille. Pour en savoir plus sur les objets trait, rendez-vous à la section du chapitre 17.

Maintenant, penchons-nous sur les macros !