ExamplesBy LevelBy TopicLearning Paths
513 Intermediate

Closure Strategy Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Strategy Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The classic Strategy pattern (GoF) uses an interface with multiple implementations: `SortStrategy`, `PriceStrategy`. Key difference from OCaml: 1. **Struct vs. record**: Rust uses a `struct` with `Box<dyn Fn>` fields; OCaml uses a record with function fields — semantically identical, syntactically different.

Tutorial

The Problem

The classic Strategy pattern (GoF) uses an interface with multiple implementations: SortStrategy, PriceStrategy. In languages without closures, this requires class hierarchies. In Rust, a Box<dyn Fn(&T, &T) -> Ordering> is a strategy — any comparator logic works, including closures created inline. This eliminates boilerplate: no traits, no implementations, no dispatch indirection beyond what Box<dyn Fn> already provides. The pattern applies to: sorting, pricing rules, validation, logging, retry policies, and any algorithm that varies independently of the structure using it.

🎯 Learning Outcomes

  • • Store a strategy as Box<dyn Fn> in a struct field
  • • Accept impl Fn + 'static in constructors and box internally
  • • Apply the strategy via (self.strategy)(args)
  • • Build factory functions for common strategies (no_discount, percentage_discount, fixed_discount)
  • • Compose multiple validation rules in a Validator that collects all errors
  • Code Example

    pub struct Sorter<T> {
        compare: Box<dyn Fn(&T, &T) -> Ordering>,
    }
    
    impl<T: Clone> Sorter<T> {
        pub fn new(compare: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
            Sorter { compare: Box::new(compare) }
        }
    }

    Key Differences

  • Struct vs. record: Rust uses a struct with Box<dyn Fn> fields; OCaml uses a record with function fields — semantically identical, syntactically different.
  • **'static bound**: Rust's impl Fn + 'static prevents strategies from capturing references to local variables with finite lifetimes; OCaml's GC manages all lifetimes.
  • Trait objects vs. closures: The classic OOP strategy uses a trait (trait PricingStrategy) with separate struct NoDiscount, struct PercentageDiscount implementations; the closure approach collapses both into a single Box<dyn Fn>.
  • Validator composition: Rust's Validator stores a Vec<Box<dyn Fn(&T) -> Result<(), String>>> collecting all rule failures; OCaml would use List.filter_map over a list of validation functions.
  • OCaml Approach

    OCaml's first-class functions make the strategy pattern trivial:

    let sort compare data = List.sort compare data
    let sorter_asc = sort compare
    let sorter_desc = sort (fun a b -> compare b a)
    
    type 'a price_calc = { discount: float -> float }
    let percentage_discount pct = { discount = fun p -> p *. (1.0 -. pct /. 100.0) }
    let fixed_discount amt = { discount = fun p -> max 0.0 (p -. amt) }
    

    OCaml's List.sort already accepts a comparator — no wrapper struct is needed. Records with function fields serve as lightweight strategy objects.

    Full Source

    #![allow(clippy::all)]
    //! Strategy Pattern via Closures
    //!
    //! Interchangeable algorithms as closure parameters and struct fields.
    
    use std::cmp::Ordering;
    
    /// Sorter with configurable comparison strategy.
    pub struct Sorter<T> {
        compare: Box<dyn Fn(&T, &T) -> Ordering>,
    }
    
    impl<T: Clone> Sorter<T> {
        pub fn new(compare: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
            Sorter {
                compare: Box::new(compare),
            }
        }
    
        pub fn sort(&self, mut data: Vec<T>) -> Vec<T> {
            data.sort_by(|a, b| (self.compare)(a, b));
            data
        }
    }
    
    /// Pricing with configurable discount strategy.
    pub struct PriceCalculator {
        discount: Box<dyn Fn(f64) -> f64>,
    }
    
    impl PriceCalculator {
        pub fn new(discount: impl Fn(f64) -> f64 + 'static) -> Self {
            PriceCalculator {
                discount: Box::new(discount),
            }
        }
    
        pub fn calculate(&self, base_price: f64) -> f64 {
            (self.discount)(base_price)
        }
    }
    
    /// Common discount strategies.
    pub fn no_discount() -> impl Fn(f64) -> f64 {
        |price| price
    }
    
    pub fn percentage_discount(pct: f64) -> impl Fn(f64) -> f64 {
        move |price| price * (1.0 - pct / 100.0)
    }
    
    pub fn fixed_discount(amount: f64) -> impl Fn(f64) -> f64 {
        move |price| (price - amount).max(0.0)
    }
    
    /// Validator with configurable validation strategy.
    pub struct Validator<T> {
        rules: Vec<Box<dyn Fn(&T) -> Result<(), String>>>,
    }
    
    impl<T> Validator<T> {
        pub fn new() -> Self {
            Validator { rules: Vec::new() }
        }
    
        pub fn add_rule(mut self, rule: impl Fn(&T) -> Result<(), String> + 'static) -> Self {
            self.rules.push(Box::new(rule));
            self
        }
    
        pub fn validate(&self, value: &T) -> Result<(), Vec<String>> {
            let errors: Vec<String> = self
                .rules
                .iter()
                .filter_map(|rule| rule(value).err())
                .collect();
    
            if errors.is_empty() {
                Ok(())
            } else {
                Err(errors)
            }
        }
    }
    
    impl<T> Default for Validator<T> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_sorter_ascending() {
            let sorter = Sorter::new(|a: &i32, b: &i32| a.cmp(b));
            assert_eq!(sorter.sort(vec![3, 1, 4, 1, 5]), vec![1, 1, 3, 4, 5]);
        }
    
        #[test]
        fn test_sorter_descending() {
            let sorter = Sorter::new(|a: &i32, b: &i32| b.cmp(a));
            assert_eq!(sorter.sort(vec![3, 1, 4, 1, 5]), vec![5, 4, 3, 1, 1]);
        }
    
        #[test]
        fn test_sorter_by_length() {
            let sorter = Sorter::new(|a: &String, b: &String| a.len().cmp(&b.len()));
            let result = sorter.sort(vec!["aaa".into(), "b".into(), "cc".into()]);
            assert_eq!(result, vec!["b", "cc", "aaa"]);
        }
    
        #[test]
        fn test_price_no_discount() {
            let calc = PriceCalculator::new(no_discount());
            assert!((calc.calculate(100.0) - 100.0).abs() < 0.001);
        }
    
        #[test]
        fn test_price_percentage_discount() {
            let calc = PriceCalculator::new(percentage_discount(20.0));
            assert!((calc.calculate(100.0) - 80.0).abs() < 0.001);
        }
    
        #[test]
        fn test_price_fixed_discount() {
            let calc = PriceCalculator::new(fixed_discount(15.0));
            assert!((calc.calculate(100.0) - 85.0).abs() < 0.001);
        }
    
        #[test]
        fn test_validator_passes() {
            let validator = Validator::new()
                .add_rule(|s: &String| {
                    if s.len() >= 3 {
                        Ok(())
                    } else {
                        Err("too short".into())
                    }
                })
                .add_rule(|s: &String| {
                    if s.chars().all(|c| c.is_alphanumeric()) {
                        Ok(())
                    } else {
                        Err("invalid chars".into())
                    }
                });
    
            assert!(validator.validate(&"hello".to_string()).is_ok());
        }
    
        #[test]
        fn test_validator_fails() {
            let validator = Validator::new().add_rule(|n: &i32| {
                if *n > 0 {
                    Ok(())
                } else {
                    Err("must be positive".into())
                }
            });
    
            assert!(validator.validate(&-5).is_err());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_sorter_ascending() {
            let sorter = Sorter::new(|a: &i32, b: &i32| a.cmp(b));
            assert_eq!(sorter.sort(vec![3, 1, 4, 1, 5]), vec![1, 1, 3, 4, 5]);
        }
    
        #[test]
        fn test_sorter_descending() {
            let sorter = Sorter::new(|a: &i32, b: &i32| b.cmp(a));
            assert_eq!(sorter.sort(vec![3, 1, 4, 1, 5]), vec![5, 4, 3, 1, 1]);
        }
    
        #[test]
        fn test_sorter_by_length() {
            let sorter = Sorter::new(|a: &String, b: &String| a.len().cmp(&b.len()));
            let result = sorter.sort(vec!["aaa".into(), "b".into(), "cc".into()]);
            assert_eq!(result, vec!["b", "cc", "aaa"]);
        }
    
        #[test]
        fn test_price_no_discount() {
            let calc = PriceCalculator::new(no_discount());
            assert!((calc.calculate(100.0) - 100.0).abs() < 0.001);
        }
    
        #[test]
        fn test_price_percentage_discount() {
            let calc = PriceCalculator::new(percentage_discount(20.0));
            assert!((calc.calculate(100.0) - 80.0).abs() < 0.001);
        }
    
        #[test]
        fn test_price_fixed_discount() {
            let calc = PriceCalculator::new(fixed_discount(15.0));
            assert!((calc.calculate(100.0) - 85.0).abs() < 0.001);
        }
    
        #[test]
        fn test_validator_passes() {
            let validator = Validator::new()
                .add_rule(|s: &String| {
                    if s.len() >= 3 {
                        Ok(())
                    } else {
                        Err("too short".into())
                    }
                })
                .add_rule(|s: &String| {
                    if s.chars().all(|c| c.is_alphanumeric()) {
                        Ok(())
                    } else {
                        Err("invalid chars".into())
                    }
                });
    
            assert!(validator.validate(&"hello".to_string()).is_ok());
        }
    
        #[test]
        fn test_validator_fails() {
            let validator = Validator::new().add_rule(|n: &i32| {
                if *n > 0 {
                    Ok(())
                } else {
                    Err("must be positive".into())
                }
            });
    
            assert!(validator.validate(&-5).is_err());
        }
    }

    Deep Comparison

    OCaml vs Rust: Strategy Pattern

    OCaml

    type 'a sorter = { compare: 'a -> 'a -> int }
    
    let make_sorter compare = { compare }
    let sort sorter data = List.sort sorter.compare data
    
    let ascending = make_sorter compare
    let descending = make_sorter (fun a b -> compare b a)
    

    Rust

    pub struct Sorter<T> {
        compare: Box<dyn Fn(&T, &T) -> Ordering>,
    }
    
    impl<T: Clone> Sorter<T> {
        pub fn new(compare: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
            Sorter { compare: Box::new(compare) }
        }
    }
    

    Key Differences

  • OCaml: Functions as record fields are natural
  • Rust: Need Box<dyn Fn> for runtime polymorphism
  • Both: Strategy is just a function/closure stored in struct
  • Rust: Can use generics with impl Fn for compile-time dispatch
  • Both enable swapping algorithms without inheritance
  • Exercises

  • Swap strategies at runtime: Implement fn PriceCalculator::set_strategy(&mut self, new: impl Fn(f64)->f64 + 'static) and verify that the new strategy takes effect immediately.
  • Validation pipeline: Extend Validator<String> with rules: must_not_be_empty, max_length(n), must_match_regex(pattern) — all composed at runtime and all errors collected.
  • Strategy registry: Build a HashMap<String, Box<dyn Fn(f64) -> f64>> discount registry and implement a apply_named_discount(name: &str, price: f64) -> Option<f64> lookup.
  • Open Source Repos