November 8, 202512 min

Higher-Order Functions de Rust: Alimenter des Closures Flexibles

m
mayo

Les higher-order functions (HOFs) en Rust—fonctions qui acceptent ou retournent d'autres functions/closures—exploitent le système de closures de Rust, les trait bounds (Fn, FnMut, FnOnce), et le modèle d'ownership pour permettre des patterns puissants de programmation fonctionnelle comme les callbacks et decorators. Je vais expliquer comment les HOFs fonctionnent en Rust, leurs mécaniques, et cas d'usage pratiques.

Que sont les Higher-Order Functions ?

Les HOFs soit :

  • Acceptent une ou plusieurs functions/closures comme arguments, ou
  • Retournent une function/closure.

Le support de Rust pour les HOFs est basé sur son système de closures, qui s'intègre parfaitement avec l'ownership, les traits, et les lifetimes.

Exemple : Function Retournant une Closure

Une fonction qui retourne une closure "adder" configurable :

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    // `move` transfère ownership de `x` dans la closure
    move |y| x + y
}

fn main() {
    let add_five = make_adder(5); // Retourne une closure qui ajoute 5
    println!("{}", add_five(3)); // 8
}

Mécaniques Clés

  • Capture de Closure : Le mot-clé move assure que la closure possède x, évitant les problèmes de lifetime après que make_adder se termine. Sans move, emprunter x causerait une erreur de compilation à cause du scope de x qui se termine.
  • Type de Retour : impl Fn(i32) -> i32 spécifie que la closure implémente le trait Fn. Chaque closure a un type anonyme unique, donc impl Trait est utilisé pour l'abstraire.

Exemple Avancé : Retour de Closure Conditionnel

Pour un comportement dynamique, retourne un Box<dyn Fn> pour supporter différentes closures au runtime :

fn math_op(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
    match op {
        "add" => Box::new(|x, y| x + y),
        "mul" => Box::new(|x, y| x * y),
        "sub" => Box::new(|x, y| x - y),
        "div" => Box::new(|x, y| if y != 0 { x / y } else { 0 }),
        _ => panic!("Opération non supportée: {}", op),
    }
}

fn main() {
    let add = math_op("add");
    let mul = math_op("mul");
    let sub = math_op("sub");
    
    println!("Add: {}", add(2, 3)); // 5
    println!("Mul: {}", mul(2, 3)); // 6
    println!("Sub: {}", sub(5, 2)); // 3
}

Ceci utilise le dynamic dispatch pour gérer des types de closures variés, idéal pour des systèmes de type plugin.

Exemples Avancés de HOFs

1. Composition de Functions

// Compose deux functions
fn compose<F, G, T, U, V>(f: F, g: G) -> impl Fn(T) -> V
where
    F: Fn(T) -> U,
    G: Fn(U) -> V,
{
    move |x| g(f(x))
}

fn main() {
    let add_one = |x: i32| x + 1;
    let double = |x: i32| x * 2;
    let square = |x: i32| x * x;
    
    // Compose: add_one puis double
    let add_then_double = compose(add_one, double);
    println!("(5 + 1) * 2 = {}", add_then_double(5)); // 12
    
    // Compose: double puis square
    let double_then_square = compose(double, square);
    println!("(5 * 2)² = {}", double_then_square(5)); // 100
    
    // Triple composition
    let complex = compose(add_one, compose(double, square));
    println!("((5 + 1) * 2)² = {}", complex(5)); // 144
}

2. Curry Functions

// Currying : transform une function à 2 paramètres en functions imbriquées
fn curry<F, A, B, C>(f: F) -> impl Fn(A) -> impl Fn(B) -> C
where
    F: Fn(A, B) -> C,
{
    move |a| move |b| f(a, b)
}

fn main() {
    let add = |x: i32, y: i32| x + y;
    let multiply = |x: i32, y: i32| x * y;
    
    // Curry add
    let curried_add = curry(add);
    let add_five = curried_add(5);
    println!("5 + 3 = {}", add_five(3)); // 8
    println!("5 + 7 = {}", add_five(7)); // 12
    
    // Curry multiply
    let curried_mul = curry(multiply);
    let double = curried_mul(2);
    let triple = curried_mul(3);
    
    println!("Double 4 = {}", double(4)); // 8
    println!("Triple 4 = {}", triple(4)); // 12
}

3. Decorators et Middleware

use std::time::{Duration, Instant};

// Decorator pour mesurer le temps d'exécution
fn time_it<F, T>(f: F) -> impl Fn() -> T
where
    F: Fn() -> T,
{
    move || {
        let start = Instant::now();
        let result = f();
        let duration = start.elapsed();
        println!("Temps d'exécution: {:?}", duration);
        result
    }
}

// Decorator pour retry logic
fn with_retry<F, T, E>(f: F, max_attempts: usize) -> impl Fn() -> Result<T, E>
where
    F: Fn() -> Result<T, E>,
    E: std::fmt::Debug,
{
    move || {
        for attempt in 1..=max_attempts {
            match f() {
                Ok(result) => return Ok(result),
                Err(e) if attempt == max_attempts => return Err(e),
                Err(e) => {
                    println!("Tentative {} échouée: {:?}", attempt, e);
                    std::thread::sleep(Duration::from_millis(100 * attempt as u64));
                }
            }
        }
        unreachable!()
    }
}

// Decorator pour logging
fn with_logging<F, T>(name: &str, f: F) -> impl Fn() -> T
where
    F: Fn() -> T,
{
    let name = name.to_string();
    move || {
        println!("🚀 Début de {}", name);
        let result = f();
        println!("✅ Fin de {}", name);
        result
    }
}

fn decorator_example() {
    // Function simulant du travail
    let expensive_work = || {
        std::thread::sleep(Duration::from_millis(200));
        42
    };
    
    // Application de multiple decorators
    let decorated = with_logging("travail coûteux", 
        time_it(expensive_work)
    );
    
    let result = decorated();
    println!("Résultat: {}", result);
    
    // Exemple avec retry
    let flaky_operation = || -> Result<String, &'static str> {
        use rand::Rng;
        if rand::thread_rng().gen_bool(0.7) {
            Err("Opération échouée")
        } else {
            Ok("Succès!".to_string())
        }
    };
    
    let reliable_operation = with_retry(flaky_operation, 3);
    match reliable_operation() {
        Ok(result) => println!("Opération réussie: {}", result),
        Err(e) => println!("Opération échouée après retries: {:?}", e),
    }
}

4. Factory Pattern avec HOFs

use std::collections::HashMap;

// Factory pour créer différents types de validators
fn create_validator(rule: &str) -> Box<dyn Fn(&str) -> bool> {
    match rule {
        "email" => Box::new(|s| s.contains('@') && s.contains('.')),
        "non_empty" => Box::new(|s| !s.is_empty()),
        "min_length" => Box::new(|s| s.len() >= 5),
        "max_length" => Box::new(|s| s.len() <= 50),
        "alphanumeric" => Box::new(|s| s.chars().all(|c| c.is_alphanumeric())),
        _ => Box::new(|_| true), // Default: toujours valide
    }
}

// Compose plusieurs validators
fn combine_validators(validators: Vec<Box<dyn Fn(&str) -> bool>>) -> impl Fn(&str) -> bool {
    move |input| {
        validators.iter().all(|validator| validator(input))
    }
}

fn validation_example() {
    // Créer des validators individuels
    let email_validator = create_validator("email");
    let non_empty_validator = create_validator("non_empty");
    let min_length_validator = create_validator("min_length");
    
    // Combine les validators
    let combined = combine_validators(vec![
        non_empty_validator,
        min_length_validator,
        email_validator,
    ]);
    
    // Test des inputs
    let test_cases = vec![
        "",
        "test",
        "test@example.com",
        "a@b.c",
        "valid.email@domain.com",
    ];
    
    for input in test_cases {
        let is_valid = combined(input);
        println!("'{}' -> {}", input, if is_valid { "✅ Valide" } else { "❌ Invalide" });
    }
}

Cas d'Usage pour les HOFs

1. Iterator Adaptors

Les closures alimentent les méthodes d'iterator comme map, filter, et fold :

fn iterator_hof_examples() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // Map avec closure
    let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
    println!("Doubled: {:?}", doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
    
    // Filter avec condition complexe
    let even_squares: Vec<_> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)
        .map(|x| x * x)
        .collect();
    println!("Even squares: {:?}", even_squares); // [4, 16, 36, 64, 100]
    
    // Fold avec closure stateful
    let sum_of_squares = numbers
        .iter()
        .fold(0, |acc, x| acc + x * x);
    println!("Sum of squares: {}", sum_of_squares); // 385
    
    // Chain operations
    let result: Vec<_> = numbers
        .iter()
        .filter(|&&x| x > 3)
        .map(|x| x * 2)
        .take(3)
        .collect();
    println!("Filtered, mapped, taken: {:?}", result); // [8, 10, 12]
}

2. Event Systems et Callbacks

use std::collections::HashMap;

struct EventEmitter {
    listeners: HashMap<String, Vec<Box<dyn Fn(&str)>>>,
}

impl EventEmitter {
    fn new() -> Self {
        Self {
            listeners: HashMap::new(),
        }
    }
    
    fn on<F>(&mut self, event: &str, callback: F)
    where
        F: Fn(&str) + 'static,
    {
        self.listeners
            .entry(event.to_string())
            .or_insert_with(Vec::new)
            .push(Box::new(callback));
    }
    
    fn emit(&self, event: &str, data: &str) {
        if let Some(callbacks) = self.listeners.get(event) {
            for callback in callbacks {
                callback(data);
            }
        }
    }
    
    // HOF pour créer des listeners avec middleware
    fn on_with_middleware<F, M>(&mut self, event: &str, middleware: M, callback: F)
    where
        F: Fn(&str) + 'static,
        M: Fn(&str, Box<dyn Fn(&str)>) + 'static,
    {
        let wrapped_callback = move |data: &str| {
            middleware(data, Box::new(&callback));
        };
        
        self.on(event, wrapped_callback);
    }
}

fn event_system_example() {
    let mut emitter = EventEmitter::new();
    
    // Simple callback
    emitter.on("user_login", |user| {
        println!("👤 User logged in: {}", user);
    });
    
    // Callback avec logging
    emitter.on("user_login", |user| {
        println!("📊 Analytics: Login event for {}", user);
    });
    
    // Multiple events
    emitter.on("user_logout", |user| {
        println!("👋 User logged out: {}", user);
    });
    
    // Emit events
    emitter.emit("user_login", "alice");
    emitter.emit("user_login", "bob");
    emitter.emit("user_logout", "alice");
}

3. Pipeline Processing

// Pipeline pour traiter des données avec multiple étapes
struct Pipeline<T> {
    steps: Vec<Box<dyn Fn(T) -> T>>,
}

impl<T: 'static> Pipeline<T> {
    fn new() -> Self {
        Self { steps: Vec::new() }
    }
    
    fn add_step<F>(mut self, step: F) -> Self
    where
        F: Fn(T) -> T + 'static,
    {
        self.steps.push(Box::new(step));
        self
    }
    
    fn execute(self, input: T) -> T {
        self.steps.into_iter().fold(input, |acc, step| step(acc))
    }
}

// HOF pour créer des étapes de transformation
fn create_transformer(operation: &str) -> Box<dyn Fn(i32) -> i32> {
    match operation {
        "double" => Box::new(|x| x * 2),
        "square" => Box::new(|x| x * x),
        "increment" => Box::new(|x| x + 1),
        "abs" => Box::new(|x| x.abs()),
        _ => Box::new(|x| x),
    }
}

fn pipeline_example() {
    let pipeline = Pipeline::new()
        .add_step(|x: i32| x + 10)        // Add 10
        .add_step(|x| x * 2)              // Double
        .add_step(|x| if x > 50 { x - 20 } else { x }) // Conditional
        .add_step(|x| x.min(100));        // Cap at 100
    
    let input = 15;
    let result = pipeline.execute(input);
    println!("Pipeline: {} -> {}", input, result); // 15 -> 30
    
    // Pipeline avec transformers dynamiques
    let dynamic_pipeline = Pipeline::new()
        .add_step(*create_transformer("increment"))
        .add_step(*create_transformer("square"))
        .add_step(*create_transformer("double"));
    
    let result2 = dynamic_pipeline.execute(5);
    println!("Dynamic pipeline: 5 -> {}", result2); // 5 -> 72 ((5+1)² * 2)
}

Performance et Optimisations

1. Static vs Dynamic Dispatch

use std::time::Instant;

// Static dispatch - plus rapide
fn apply_static<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(x)
}

// Dynamic dispatch - plus flexible
fn apply_dynamic(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn performance_comparison() {
    let operation = |x: i32| x * 2 + 1;
    let iterations = 10_000_000;
    
    // Test static dispatch
    let start = Instant::now();
    for i in 0..iterations {
        apply_static(&operation, i);
    }
    let static_time = start.elapsed();
    
    // Test dynamic dispatch
    let start = Instant::now();
    for i in 0..iterations {
        apply_dynamic(&operation, i);
    }
    let dynamic_time = start.elapsed();
    
    println!("Static dispatch: {:?}", static_time);
    println!("Dynamic dispatch: {:?}", dynamic_time);
    println!("Ratio: {:.2}x", dynamic_time.as_nanos() as f64 / static_time.as_nanos() as f64);
}

2. Éviter les Allocations Inutiles

// ❌ Mauvais - allocations répétées
fn bad_hof_pattern() -> Vec<Box<dyn Fn(i32) -> i32>> {
    let mut transformers = Vec::new();
    for i in 1..=5 {
        transformers.push(Box::new(move |x| x * i)); // Allocation pour chaque closure
    }
    transformers
}

// ✅ Bon - structure plus efficace
fn good_hof_pattern() -> impl Fn(i32) -> i32 {
    |x| (1..=5).fold(x, |acc, i| acc * i) // Pas d'allocations
}

Points Clés

Les HOFs permettent des patterns flexibles et réutilisables en traitant les functions comme des valeurs de première classe.
Utilise impl Fn pour le static dispatch zero-cost dans le code critique en performance.
Utilise Box<dyn Fn> pour un comportement dynamique avec multiple types de closures.
🚀 Utilise move pour assurer que les closures possèdent les données capturées quand retournées.

Règles de Décision

  1. Performance critiqueimpl Fn (static dispatch)
  2. Comportement dynamiqueBox<dyn Fn> (dynamic dispatch)
  3. Composition simple → Functions inline
  4. Logic complexe → Structs avec méthodes
  5. APIs publiquesimpl Trait pour flexibilité

Exemple Réel : Les HOFs sont centrales à l'API iterator de Rust (map, filter) et aux frameworks async comme tokio, où les closures définissent le comportement des tâches.

Expérimente : Modifie make_adder pour retourner une closure qui multiplie au lieu d'additionner.
Réponse : Le compilateur l'accepte parfaitement, car les deux closures implémentent Fn(i32) -> i32, maintenant la cohérence de type.

Exemple Pratique Complet

use std::collections::HashMap;

// Système de traitement de commandes avec HOFs
struct OrderProcessor {
    validators: Vec<Box<dyn Fn(&Order) -> Result<(), String>>>,
    transformers: Vec<Box<dyn Fn(Order) -> Order>>,
    handlers: HashMap<String, Box<dyn Fn(&Order)>>,
}

#[derive(Debug, Clone)]
struct Order {
    id: String,
    amount: f64,
    customer: String,
    items: Vec<String>,
}

impl OrderProcessor {
    fn new() -> Self {
        Self {
            validators: Vec::new(),
            transformers: Vec::new(),
            handlers: HashMap::new(),
        }
    }
    
    // HOF pour ajouter des validators
    fn with_validator<F>(mut self, validator: F) -> Self
    where
        F: Fn(&Order) -> Result<(), String> + 'static,
    {
        self.validators.push(Box::new(validator));
        self
    }
    
    // HOF pour ajouter des transformers
    fn with_transformer<F>(mut self, transformer: F) -> Self
    where
        F: Fn(Order) -> Order + 'static,
    {
        self.transformers.push(Box::new(transformer));
        self
    }
    
    // HOF pour ajouter des handlers
    fn with_handler<F>(mut self, event: &str, handler: F) -> Self
    where
        F: Fn(&Order) + 'static,
    {
        self.handlers.insert(event.to_string(), Box::new(handler));
        self
    }
    
    fn process(&self, mut order: Order) -> Result<Order, String> {
        // Validation
        for validator in &self.validators {
            validator(&order)?;
        }
        
        // Transformation
        for transformer in &self.transformers {
            order = transformer(order);
        }
        
        // Event handling
        if let Some(handler) = self.handlers.get("processed") {
            handler(&order);
        }
        
        Ok(order)
    }
}

fn order_processing_example() {
    let processor = OrderProcessor::new()
        .with_validator(|order| {
            if order.amount <= 0.0 {
                Err("Le montant doit être positif".to_string())
            } else {
                Ok(())
            }
        })
        .with_validator(|order| {
            if order.customer.is_empty() {
                Err("Le client est requis".to_string())
            } else {
                Ok(())
            }
        })
        .with_transformer(|mut order| {
            // Ajouter des frais de service
            order.amount = (order.amount * 1.05).round() / 100.0 * 100.0;
            order
        })
        .with_transformer(|mut order| {
            // Normaliser le nom du client
            order.customer = order.customer.to_uppercase();
            order
        })
        .with_handler("processed", |order| {
            println!("📦 Commande traitée: {} pour {} ({}€)", 
                     order.id, order.customer, order.amount);
        });
    
    let order = Order {
        id: "ORD-001".to_string(),
        amount: 99.99,
        customer: "alice dupont".to_string(),
        items: vec!["item1".to_string(), "item2".to_string()],
    };
    
    match processor.process(order) {
        Ok(processed_order) => {
            println!("✅ Commande traitée avec succès: {:?}", processed_order);
        }
        Err(e) => {
            println!("❌ Erreur de traitement: {}", e);
        }
    }
}

fn main() {
    decorator_example();
    println!("---");
    validation_example();
    println!("---");
    iterator_hof_examples();
    println!("---");
    event_system_example();
    println!("---");
    pipeline_example();
    println!("---");
    order_processing_example();
}

Conclusion : Les higher-order functions en Rust offrent une puissance énorme pour créer des abstractions flexibles et réutilisables. Maîtrise-les pour écrire du code fonctionnel expressif et performant !

Retour au blog
Partager ::