4. Itérateurs et Closures

Les itérateurs en Rust sont paresseux et composables. Combinés aux closures, ils permettent d’écrire du code de traitement de données concis, sûr et performant (zéro-coût par rapport à une boucle manuelle).


4.1 Closures

Une closure est une fonction anonyme qui peut capturer son environnement.

Syntaxe

let add_one = |x: i32| -> i32 { x + 1 };
let add_one = |x| x + 1;              // types inférés
let add_one = |x| x + 1;              // encore plus court
 
println!("{}", add_one(41));  // 42

Capture de l’environnement

Les closures peuvent capturer des variables de leur portée. Le compilateur choisit automatiquement la méthode de capture la moins restrictive.

let factor = 2.0;
let scale = |x: f64| x * factor;   // capture factor par référence (&f64)
 
let mut values = vec![1, 2, 3];
let mut push = |v| values.push(v);  // capture par référence mutable (&mut Vec)
 
let s = String::from("hello");
let consume = || drop(s);           // capture par move (s est déplacé)
// println!("{s}");                  // ⛕ s a été moved dans la closure
 
// Forcer le move avec le mot-clé move :
let data = vec![1, 2, 3];
let handler = move || {             // data est moved dans la closure
    println!("{:?}", data);
};

Closures et fonctions

Une closure qui ne capture rien peut être utilisée comme une fonction :

fn add_one_fn(x: i32) -> i32 { x + 1 }
 
let add_one_closure = |x| x + 1;
 
fn apply(f: fn(i32) -> i32, x: i32) -> i32 { f(x) }
 
apply(add_one_fn, 41);        // ✅ fn pointer
apply(add_one_closure, 41);   // ✅ aussi (si pas de capture)

4.2 Itérateurs

Un itérateur est n’importe quel type qui implémente le trait Iterator :

pub trait Iterator {
    type Item;              // type des éléments produits
    fn next(&mut self) -> Option<Self::Item>;  // méthode obligatoire
}

Créer un itérateur

// Sur les collections
let v = vec![1, 2, 3];
let iter = v.iter();          // &T (références immutables)
let iter = v.iter_mut();      // &mut T (références mutables)
let iter = v.into_iter();     // T (consomme la collection)
 
// Plages
let iter = 0..10;
let iter = 0..=10;            // inclusif (0 à 10)
 
// FromIterator (collect)
let vec: Vec<i32> = (0..5).collect();

Adaptateurs paresseux

Les adaptateurs transforment un itérateur en un autre itérateur. Ils sont paresseux — rien n’est exécuté tant qu’on n’appelle pas next() ou collect().

let v = vec![1, 2, 3, 4, 5];
 
// map : transforme chaque élément
let doubled: Vec<i32> = v.iter()
    .map(|x| x * 2)
    .collect();
 
// filter : garde les éléments selon un prédicat
let evens: Vec<&i32> = v.iter()
    .filter(|x| *x % 2 == 0)
    .collect();
 
// filter_map : combinaison filter + map (Option)
// Utile pour .ok() : Result → Option
let strings = vec!["42", "hello", "37"];
let parsed: Vec<i32> = strings.iter()
    .filter_map(|s| s.parse().ok())
    .collect();  // [42, 37]
 
// flat_map : chaque élément produit un itérateur, aplati
let words = vec!["hello world", "rust is cool"];
let all_words: Vec<&str> = words.iter()
    .flat_map(|s| s.split_whitespace())
    .collect();  // ["hello", "world", "rust", "is", "cool"]
 
// take et skip
let first_3: Vec<&i32> = v.iter().take(3).collect();  // [1, 2, 3]
 
// enumerate
for (i, val) in v.iter().enumerate() {
    println!("index {i}: {val}");
}
 
// zip : combine deux itérateurs
let keys = vec!["a", "b", "c"];
let values = vec![1, 2, 3];
let pairs: Vec<(&str, i32)> = keys.iter()
    .zip(values.iter())
    .map(|(k, v)| (*k, *v))
    .collect();  // [("a", 1), ("b", 2), ("c", 3)]

Consommateurs d’itérateurs

let v = vec![1, 2, 3, 4, 5];
 
// sum, product
let sum: i32 = v.iter().sum();
let product: i32 = v.iter().product();
 
// count, max, min
let count = v.iter().count();
let max = v.iter().max();       // Option<&i32>
let min = v.iter().min();
 
// any, all (prédicats)
let has_even = v.iter().any(|x| x % 2 == 0);
let all_positive = v.iter().all(|x| x > 0);
 
// fold (réduction générale)
let sum = v.iter().fold(0, |acc, x| acc + x);
 
// position, rposition
let pos = v.iter().position(|x| x == &3);  // Option<usize>
 
// find
let found = v.iter().find(|x| **x > 3);    // Option<&i32>

4.3 Exemple Concret : Médiane sur Itérateur

fn median(gradients: &[f64]) -> Option<f64> {
    let n = gradients.len();
    if n == 0 {
        return None;
    }
 
    // Trier avec un itérateur (sans copie inutile)
    let mut sorted: Vec<f64> = gradients.iter().copied().collect();
    sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
 
    let mid = n / 2;
    if n % 2 == 0 {
        Some((sorted[mid - 1] + sorted[mid]) / 2.0)
    } else {
        Some(sorted[mid])
    }
}
 
fn trimmed_mean(gradients: &[f64], trim_ratio: f64) -> Option<f64> {
    let n = gradients.len();
    if n == 0 {
        return None;
    }
 
    let trim_count = (n as f64 * trim_ratio).ceil() as usize;
 
    // Version itérateur
    let mut sorted: Vec<f64> = gradients.iter().copied().collect();
    sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
 
    let sum: f64 = sorted.iter()
        .skip(trim_count)           // ignore les plus petits
        .take(n - 2 * trim_count)   // prend la partie centrale
        .sum();
 
    let count = n - 2 * trim_count;
    if count == 0 { None } else { Some(sum / count as f64) }
}

4.4 Itérateurs Personnalisés

struct GradientIter<'a> {
    grads: &'a [f64],
    index: usize,
}
 
impl<'a> Iterator for GradientIter<'a> {
    type Item = &'a f64;
 
    fn next(&mut self) -> Option<Self::Item> {
        if self.index >= self.grads.len() {
            None
        } else {
            let result = &self.grads[self.index];
            self.index += 1;
            Some(result)
        }
    }
}
 
// Convertir un slice en itérateur
fn iter_gradients(grads: &[f64]) -> GradientIter<'_> {
    GradientIter { grads, index: 0 }
}
 
// Test
let g = vec![1.0, 2.0, 3.0];
for val in iter_gradients(&g) {
    println!("{val}");
}

4.5 Itérateurs et Performance (Zéro-Cost Abstraction)

Les itérateurs Rust se compilent en code machine identique à une boucle for manuelle. Pas de surcoût.

// Boucle manuelle
let mut sum = 0;
for i in 0..1000 {
    sum += i;
}
 
// Itérateur (même code assembleur !)
let sum: i32 = (0..1000).sum();

Benchmark : v.iter().map(|x| x * 2).filter(|x| x > 10).sum() produit le même assembleur que la boucle for équivalente. Pas d’allocation, pas de virtual call.


4.6 Résumé

AdaptateurEffetParesseux
.map(f)Transforme chaque élément
.filter(p)Garde selon prédicat
.filter_map(f)map + filter en un passage
.flat_map(f)Chaque élément → itérateur, aplati
.take(n)Garde les n premiers
.skip(n)Ignore les n premiers
.zip(other)Combine deux itérateurs
.enumerate()Ajoute l’index
.chain(other)Concatène deux itérateurs
.cycle()Répète à l’infini
ConsommateurEffet
.collect()Produit une collection
.sum() / .product()Somme / produit
.fold(init, f)Réduction générale
.any() / .all()Test booléen
.count() / .max() / .min()Statistiques

🔗 Voir aussi