Programmation fonctionnelle en Rust : des itérateurs aux monades
Rust est un langage multiparadigme qui combine harmonieusement les styles impératif, orienté objet et fonctionnel. Bien que Rust ne soit pas un langage purement fonctionnel (comme Haskell), son système de types et de possession permet d'écrire un code élégant, sûr et expressif dans un style fonctionnel. Dans cet article, nous allons décortiquer les concepts clés de la programmation fonctionnelle (FP) en Rust : les closures, les itérateurs, le pattern matching, ainsi que le travail avec Option et Result. Ce contenu sera utile aussi bien aux débutants qu'aux développeurs expérimentés souhaitant approfondir les capacités fonctionnelles de Rust.
Closures et expressions lambda
Les closures en Rust sont des fonctions anonymes qui peuvent capturer des variables de la portée environnante. Elles constituent le fondement de l'approche fonctionnelle, permettant de transmettre un comportement comme des données.
La syntaxe des closures est concise : |paramètres| corps. Les closures peuvent être passées à des fonctions, stockées dans des variables et retournées depuis des fonctions. Il est important de se souvenir des trois traits qui contrôlent la capture : Fn (emprunt), FnMut (emprunt mutable) et FnOnce (consommation).
fn main() { // Closure simple let add_one = |x: i32| x + 1; println!("{}", add_one(5)); // 6
// Capture de variable let factor = 10; let multiply = |x| x * factor; println!("{}", multiply(3)); // 30
// Closure avec le trait FnOnce let name = String::from("Rust"); let greet = move || { println!("Bonjour, {}!", name); // name est déplacé }; greet(); // println!("{}", name); // Erreur ! name n'est plus accessible}Le mot-clé move force la capture par valeur (déplacement), ce qui est souvent nécessaire lors du travail avec la concurrence ou lors du passage d'une closure dans un autre contexte.
Itérateurs : traitement paresseux et chaînes de méthodes
L'un des points forts du style fonctionnel en Rust est le puissant adaptateur d'itérateur. Au lieu de boucles for explicites et de mutations de variables, vous utilisez des chaînes de méthodes : map, filter, fold, collect et autres. Les itérateurs en Rust sont paresseux — ils n'effectuent pas de calculs tant qu'une méthode consommatrice n'est pas appelée (par exemple, collect() ou for_each()).
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6];
// Approche fonctionnelle : filtrage et transformation let result: Vec<i32> = numbers .iter() // création d'un itérateur .filter(|&x| x % 2 == 0) // on garde uniquement les pairs .map(|x| x * x) // mise au carré .collect(); // collecte dans un vecteur
println!("{:?}", result); // [4, 16, 36]
// Somme avec fold (réduction) let sum = numbers.iter().fold(0, |acc, x| acc + x); println!("Somme : {}", sum); // 21
// Vérification de condition avec any et all let has_even = numbers.iter().any(|x| x % 2 == 0); println!("Y a-t-il des pairs : {}", has_even); // true}L'utilisation des itérateurs rend le code déclaratif, lisible et moins sujet aux erreurs liées à l'indexation et aux limites des tableaux. Le compilateur Rust optimise souvent les chaînes d'itérateurs en code machine efficace, comparable aux boucles manuelles.
Pattern matching et déstructuration
Le pattern matching (match) est le cœur du contrôle de flux fonctionnel en Rust. Il permet de traiter en toute sécurité toutes les variantes possibles des données, en particulier en combinaison avec les types de données algébriques (enum). Les motifs peuvent être imbriqués, inclure des conditions (guards) et déstructurer des structures, des tuples et des énumérations.
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),}
fn handle_