Les closures en Rust
Introduction
Le prochain chapitre traitera des threads en langage Rust. Et pour cela, nous avons besoin d’approfondir nos connaissances sur les closures. En effet, l’écriture de threads en Rust implique de manipuler des closures, notion que nous avons brièvement croisée précédemment.
Nous avons effectivement rencontré les closures dans le chapitre Les vecteurs en langage Rust, en particulier lors de l’utilisation de la méthode retain :
vecteur_entiers.retain(|&x| x % 2 == 0);
Ce fragment de code |&x| x % 2 == 0 correspond à une closure, c’est-à-dire une expression lambda ou encore une fonction anonyme. De fait, cela fonctionne strictement comme une fonction, sans que l’on ait besoin du mot-clé fn, ni de préciser paramètres et type de retour. L’écriture est plus souple et intuitive et permet une certaine fluidité dans l’écriture du code.
Considérations théoriques
Reprenons cette ligne issue du chapitre Les vecteurs en langage Rust :
vecteur_entiers.retain(|&x| x % 2 == 0);
La méthode retain attend une lambda-expression (une closure), qui lui permet de garder les éléments du vecteur qui répondent à un certain critère.
Entre les deux barres verticales « | | », on représente une référence vers un élément du vecteur. On ne conservera que ceux qui répondent au critère, en l’occurrence celui d’être un nombre pair, exprimé par x % 2 == 0.
Si on essaie de définir une closure en Rust, on dira qu’elle correspond à ce que l’on appelle, dans divers langages, une fonction anonyme. Cela peut par exemple évoquer LINQ en langage C#. Plusieurs usages sont à noter :
-
Stocker cette fonction anonyme dans une simple variable, pour réutilisation.
-
Passer cette fonction anonyme en paramètre d’une autre fonction, comme par exemple avec retain.
Un avantage comparatif important des closures est qu’elles ont accès à des valeurs définies dans la portée dans laquelle elles sont définies. Au contraire, une simple fonction peut manipuler des paramètres qui lui sont passés, mais n’aura pas nécessairement accès à certaines variables de la portée...
Première utilisation d’une closure
Reprenons avec l’exemple de la parité d’un nombre telle qu’utilisée en paramètre de la méthode retain. Nous allons reprendre cette closure, mais en la stockant dans une simple variable, pour l’utiliser ensuite.
On crée un petit projet dédié aux closures :
cargo new closures --bin
> Created binary (application) `closures` package
On crée une closure stockée dans une variable, que l’on utilise d’emblée. La voici :
let est_un_nombre_pair = |x : i64| x % 2 == 0;
Le petit code est le suivant :
fn main() {
let est_un_nombre_pair = |x : i64| x % 2 == 0;
let quatorze : i64 = 14;
let treize : i64 = 13;
println!(" 14 est un nombre pair ? {}", est_un_nombre_pair(quatorze));
println!(" 13 est un nombre pair ? {}", est_un_nombre_pair(treize));
println!(" 12 est un nombre pair ? {}", est_un_nombre_pair(12));
}
Cette fonction main donne ceci en sortie :
> 14 est un nombre pair ? true
> 13 est un nombre pair ? false
> 12 est un nombre pair ? True
Tri facile avec une closure
Pour démontrer l’intérêt d’une closure pour le tri, prenons l’exemple suivant. On a une structure Individu qui contient deux champs : la chaîne de caractères relative au prénom et une valeur entière qui représente l’âge :
struct Individu {
prenom : String,
age : i32
}
impl Individu {
pub fn creer(prenom : String, age : i32) -> Individu {
Individu { prenom : prenom, age : age }
}
}
On crée un vecteur d’instances de cette structure. On voudrait pouvoir facilement le trier selon l’âge des individus (du plus jeune au plus âgé). Pour cela, on connaît surtout un procédé particulièrement adéquat : implémenter le trait qui convient.
On déclare l’utilisation du trait en question :
use std::cmp::Ordering;
On ajoute la ligne qui convient, relative à la structure elle-même ainsi que le trait Debug pour pouvoir facilement afficher du contenu dans la console :
#[derive(Eq, Debug)]
Nous sommes alors prêts à fournir une implémentation à même de trier des individus selon...
Les closures, résumé des premières notions
Rappelons ici la syntaxe d’une closure telle qu’utilisée précédemment :
-
On met entre barres verticales « | | » la ou les variables d’entrée de la fonction anonymes.
-
Quand l’expression est simple, il n’est pas utile de mettre l’expression de la closure entre parenthèses. Quand elle est plus élaborée, il faut placer l’expression entre accolades.
On peut résumer cette syntaxe avec la ligne ci-dessous :
let closure = |parametre_1, parametre_2| ->
type_de_retour {..... expression .....};
Voici à présent quelques remarques utiles.
-
Une des grandes forces d’une closure est son accès à des variables ou à des fonctions définies dans sa portée, c’est-à-dire en dehors de sa propre définition. Cela permet, en une ligne, de fournir un outil très performant (comme pour l’exemple du tri, plus haut). On parle parfois de capture de valeurs.
-
Les closures fonctionnent avec de l’inférence de type. Autrement dit, la closure peut interpoler le type manipulé sans que ce dernier soit précisé explicitement.
-
Les closures en Rust offrent une solution particulièrement performante. Leur usage est donc encouragé, contrairement à d’autres langages...
Considérations sur les traits FnOnce, FnMut et Fn
1. Explications du fonctionnement général
Les closures en Rust implémentent les traits FnOnce, FnMut et Fn, ce qui autorise les comportements suivants :
-
La closure devient propriétaire d’une variable, seulement une fois (FnOnce), c’est-à-dire que cela fonctionne uniquement si on n’appelle qu’une seule fois la fonction closure.
-
La closure emprunte une variable mutable (FnMut) selon &mut self.
-
La closure emprunte de façon non mutable une variable à sa portée (Fn) selon &self.
Ces trois scénarios correspondent aux différents usages que l’on peut faire d’une closure en Rust en termes d’emprunt et de propriété.
Ci-dessous la documentation en ligne de chacun de ces trois traits :
https://doc.rust-lang.org/stable/std/ops/trait.FnOnce.html
2. Le mot-clé move
Un scénario n’est pas possible avec l’implémentation de ces trois traits. Il s’agit de celui décrit ci-dessous :
« On veut une propriété effective par la fonction closure d’une variable donnée et l’on veut appeler autant de fois que l’on veut cette closure. »
On voit que l’usage attendu n’est pas permis...