Acheter version papier

Rust

Fork me on GitHub

II. Spécificités de Rust

11. Closure

Nous allons maintenant aborder un chapitre très important pour le langage Rust. Ceux ayant déjà utilisé des langages fonctionnels n'y verront qu'une révision (mais ça ne fait jamais de mal après tout !).

Pour ceux qui n'ont jamais utilisé de closures, on peut les définir comme des fonctions anonymes qui capturent leur environnement.

"Une fonction "anonyme" ? Elle "capture" son environnement ?"

Ne vous inquiétez pas, vous allez très vite comprendre, prenons un exemple simple :

Runlet multiplication = |nombre: i32, multiplicateur: i32| nombre * multiplicateur;

println!("{}", multiplication(2, 2));

Pour le moment, vous vous dites sans doute qu'en fait, ce n'est qu'une fonction. Maintenant ajoutons un élément :

Runlet nombre = 2i32;
let multiplication = |multiplicateur: i32| nombre * multiplicateur;

println!("{}", multiplication(2));

Là je pense que vous vous demandez comment il fait pour trouver la variable nombre puisqu'elle n'est pas dans le scope de la "fonction". Comme je vous l'ai dit, une closure capture son environnement, elle a donc accès à toutes les variables présentes dans le scope de la fonction qui la crée.

Mais à quoi ça peut bien servir ? Imaginons que vous ayez une interface graphique et que vous souhaitez effectuer une action lorsque l'utilisateur clique sur un bouton. Cela donnerait quelque chose dans ce genre :

Runlet mut bouton = Bouton::new();
let mut clicked = false;

bouton.clicked(|titre| {
    clicked = true;
    println!("On a cliqué sur le bouton {} !", titre);
});

Très pratique pour partager des informations avec des éléments en dehors du scope de la closure sans avoir besoin d'ajouter des mécanismes qui s'en chargeraient. Les closures sont utilisées pour trier des slices par-exemple.

Si jamais vous souhaitez écrire une fonction recevant une closure en paramètre, voici à quoi cela va ressembler :

Runfn fonction_avec_closure<F>(closure: F) -> i32
    where F: Fn(i32) -> i32
{
    closure(1)
}

Ici, la closure prend un i32 comme paramètre et renvoie un i32. Vous remarquerez que la syntaxe est proche de celle d'une fonction générique, la seule différence venant du mot-clé where qui permet de définir à quoi doit ressembler la closure. À noter qu'on aurait aussi pu écrire la fonction de cette façon :

Runfn fonction_avec_closure<F: Fn(i32) -> i32>(closure: F) -> i32 {
    closure(1)
}

Chose intéressante à noter : le trait Fn est implémenté sur les closures… mais aussi sur les fonctions ! Un générique qui accepte une closure acceptera aussi une fonction. Nous pourrions donc faire :

Runfn fonction_avec_closure<F: Fn(i32) -> i32>(closure: F) -> i32 {
    closure(1)
}

// On définit qui correspond à la définition du générique "F" de
// "fonction_avec_closure".
fn fonction(nb: i32) -> i32 {
    nb * 2
}

// Les 2 appels font exactement la même chose.
fonction_avec_closure(|nb: i32| nb * 2);
fonction_avec_closure(fonction);

Nous avons maintenant vu les closures de type Fn. Il en existe cependant deux autres types avec chacune ses propres caractéristiques.

FnMut

Si jamais vous souhaitez avoir un accès mutable sur une variable capturée dans une closure, il vous faudra utiliser le trait FnMut :

Runfn appelle_2_fois<F>(mut func: F)
    where F: FnMut()
{
    func();
    func();
}

let mut x: usize = 1;
// Cette closure a besoin d'un accès mutable à la variable x.
let ajoute_deux_a_x = || x += 2;
appelle_2_fois(ajoute_deux_a_x);

assert_eq!(x, 5);

Si jamais appelle_2_fois attendait une Fn à la place, on aurait eu l'erreur suivante :

error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
closure is `FnMut` because it mutates the variable `x`

FnOnce

Voici le dernier type de closure : les closures FnOnce. Elles ne peuvent être appelées qu'une seule fois :

Runfn utilisation<F>(func: F)
    where F: FnOnce() -> String
{
    println!("Utilisation de func : {}", func());
    // On ne peut plus utiliser "func" ici.
}

let x = String::from("x");
let return_x: FnOnce() -> String = move || x;
utilisation(return_x));
// On ne peut plus utiliser "func" ici non plus puisqu'on l'a move
// dans "utilisation".

Une fonction qui prend FnOnce en argument apporte une information très intéressante : vous pouvez être sûr que cette closure ne sera appelé qu'une seule et unique fois. Si vous voulez faire une opération qui ne doit pas être exécutée plus d'une fois, c'est une garantie qui se révéler très utile.

Nous avons donc vu les bases des closures. C'est une partie importante, je vous conseille donc de bien vous entraîner dessus jusqu'à être sûr de bien les maîtriser !

Après ça, il est temps d'attaquer un chapitre un peu plus "tranquille".