Programmation fonctionnelle en Rust : niveau avancé
Rust est un langage multi-paradigme qui combine harmonieusement les approches impératives et fonctionnelles. Pour les développeurs déjà familiers avec les bases de Rust, un monde de techniques fonctionnelles avancées s'ouvre, permettant d'écrire un code plus concis, sûr et expressif. Dans cet article, nous explorerons les concepts clés de la programmation fonctionnelle (PF) à un niveau avancé : de l'utilisation approfondie des fermetures et des itérateurs à la composition de fonctions et à l'émulation de monades.
1. Fermetures avancées : possession, emprunt et traits
Les fermetures en Rust ne sont pas de simples fonctions anonymes. Le compilateur implémente automatiquement pour elles l'un des traits : Fn, FnMut ou FnOnce. Comprendre comment une fermeture capture les variables de son environnement est crucial pour écrire un code efficace.
Capture avec déplacement (move closure) est utilisée lorsque la fermeture doit posséder les données capturées, par exemple lors du passage à un nouveau thread :
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { println!("{:?}", data); // data déplacée dans la fermeture});
handle.join().unwrap();Fermetures comme arguments de fonctions permettent de créer des API flexibles. Utilisez des types génériques avec des contraintes de traits :
fn apply_twice<F>(f: F, x: i32) -> i32where F: Fn(i32) -> i32,{ f(f(x))}
let square = |x| x * x;println!("{}", apply_twice(square, 2)); // Sortie : 16Retourner des fermetures est une tâche complexe en raison de la taille inconnue du type de la fermeture. Utilisez Box<dyn Fn> ou le mot-clé impl :
fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y}
let add_five = make_adder(5);println!("{}", add_five(10)); // 152. Itérateurs : paresse, combinateurs et performance
Les itérateurs en Rust sont un puissant outil fonctionnel. Ils sont paresseux : les calculs n'ont lieu que lors de l'appel d'une méthode terminale (par exemple, collect(), for_each()).
Chaînes de combinateurs permettent de construire des transformations de données complexes sans boucles :
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers .iter() .filter(|&&x| x % 2 == 0) // on garde les pairs .map(|&x| x * 2) // on multiplie par 2 .take(2) // on prend les deux premiers .collect();
println!("{:?}", result); // [4, 8]Itérateurs personnalisés sont créés en implémentant le trait Iterator. Par exemple, un itérateur de nombres de Fibonacci :
struct Fibonacci { curr: u64, next: u64,}
impl Fibonacci { fn new() -> Self { Fibonacci { curr: 0, next: 1 } }}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<Self::Item> { let new_next = self.curr.checked_add(self.next)?; self.curr = self.next; self.next = new_next; Some(self.curr) }}
let fib: Vec<u64> = Fibonacci::new().take(10).collect();println!("{:?}", fib); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]Performance des itérateurs est souvent comparable à celle des boucles manuelles, et dans certains cas, le compilateur LLVM génère un code plus optimal.
3. Composition de fonctions et style monadique
Rust n'a pas de composition de fonctions intégrée, mais elle est facile à implémenter. La composition permet de construire de nouvelles fonctions à partir de fonctions existantes.
Composition simple à l'aide de fermetures :
fn compose<F, G, A, B, C>(f: F, g: G) -> impl Fn(A) -> Cwhere F: Fn(B) -> C, G: Fn(A) -> B,{ move |x| f(g(x))}
let add_o