Programmation fonctionnelle en Rust : de A à Z
Le langage Rust, connu pour sa gestion sécurisée de la mémoire et ses hautes performances, offre également des outils puissants pour la programmation fonctionnelle. Contrairement aux langages purement fonctionnels (Haskell), Rust permet de combiner de manière flexible les styles impératif et fonctionnel. Dans cet article, nous aborderons les concepts clés de la programmation fonctionnelle en Rust : des fermetures de base aux motifs monadiques avancés. Ce contenu sera utile aussi bien aux débutants qu'aux développeurs expérimentés souhaitant écrire un code plus concis et plus fiable.
1. Bases : Immutabilité et variables
La programmation fonctionnelle commence par l'immutabilité (l'immuabilité). En Rust, les variables sont immuables par défaut. C'est une différence clé par rapport à de nombreux langages.
// Variable immuable (par défaut)let x = 10;// x = 20; // Erreur de compilation !
// Variable mutable (explicitement)let mut y = 5;y = 15; // FonctionneL'utilisation de let sans mut garantit que la valeur ne changera pas après l'initialisation. Cela réduit le nombre d'effets secondaires et rend le code plus prévisible. Dans un style fonctionnel, il est recommandé de privilégier les variables immuables, en créant de nouvelles valeurs plutôt qu'en modifiant les anciennes.
2. Fermetures (Closures) — fonctions anonymes
Les fermetures sont des fonctions anonymes qui peuvent capturer des variables de leur environnement. En Rust, elles sont implémentées via les traits Fn, FnMut et FnOnce.
2.1. Syntaxe et capture de variables
// Fermeture simplelet add_one = |x: i32| -> i32 { x + 1 };println!("{}", add_one(5)); // 6
// Capture d'une variable de l'environnementlet base = 10;let add_base = |x| x + base; // base est capturée par référence (&)println!("{}", add_base(20)); // 30
// Capture avec mutation (FnMut)let mut counter = 0;let mut increment = || { counter += 1; };increment();increment();println!("{}", counter); // 2
// Capture avec consommation (FnOnce)let name = String::from("Rust");let consume = || { drop(name); };consume();// println!("{}", name); // Erreur : name a été déplacé2.2. Fermetures comme arguments de fonctions
Les fermetures sont souvent passées à des fonctions d'ordre supérieur, telles que map, filter et fold.
let numbers = vec![1, 2, 3, 4, 5];
// Utilisation d'une fermeture avec filterlet evens: Vec<i32> = numbers .into_iter() .filter(|&x| x % 2 == 0) .collect();
println!("{:?}", evens); // [2, 4]3. Itérateurs et chaînes de transformations
Les itérateurs sont le cœur de la programmation fonctionnelle en Rust. Ils sont paresseux (les calculs n'ont lieu qu'à l'appel de collect() ou de for) et permettent de construire des chaînes de transformations.
3.1. Adaptateurs d'itérateurs de base
let data = vec![1, 2, 3, 4, 5, 6];
// Chaîne : filter -> map -> take -> collectlet result: Vec<i32> = data .iter() .filter(|&x| x % 2 == 0) // on garde les pairs .map(|x| x * 2) // on double .take(2) // on prend les deux premiers .collect();
println!("{:?}", result); // [4, 8]3.2. Pliage (fold) et réduction
fold est l'équivalent de reduce dans d'autres langages. Il accumule un résultat en parcourant tous les éléments.
let numbers = [1, 2, 3, 4, 5];
// Somme de tous les nombreslet sum = numbers.iter().fold(0, |acc, &x| acc + x);println!("Sum: {}", sum); // 15
// Factoriellelet factorial = (1..=5).fold(1, |acc, x| acc * x);println!("5! = {}", factorial); // 1203.3. Itérateurs infinis paresseux
// Itérateur infini, on prend les 10 premierslet first_ten: Vec<i32> = (0..) .filter(|x| x % 2 == 0) .map(|x| x * x)