ExamplesBy LevelBy TopicLearning Paths
522 Intermediate

Predicate Functions Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Predicate Functions Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Predicates — boolean-valued functions — appear everywhere in data processing: filtering collections, validating inputs, routing events, enforcing business rules. Key difference from OCaml: 1. **Return type opaqueness**: Rust `impl Fn(&T)

Tutorial

The Problem

Predicates — boolean-valued functions — appear everywhere in data processing: filtering collections, validating inputs, routing events, enforcing business rules. When predicates are composed rather than inlined, code becomes self-documenting and reusable. The predicate combinator pattern (AND, OR, NOT, all_of, any_of) treats predicates as first-class values, enabling query languages, rule engines, and access-control systems to be built from small, testable pieces.

🎯 Learning Outcomes

  • • How to implement composable predicates using impl Fn(&T) -> bool return types
  • • How pred_and, pred_or, and pred_not build new predicates from existing ones
  • • How all_of and any_of generalize to dynamic lists of predicates
  • • How to combine predicate composition with Iterator::filter for expressive queries
  • • How the pattern maps to rule engines and validation frameworks in production systems
  • Code Example

    pub fn pred_and<T, P1, P2>(p1: P1, p2: P2) -> impl Fn(&T) -> bool
    where P1: Fn(&T) -> bool, P2: Fn(&T) -> bool {
        move |x| p1(x) && p2(x)
    }
    
    let is_positive_even = pred_and(is_positive(), is_even());

    Key Differences

  • Return type opaqueness: Rust impl Fn(&T) -> bool hides the concrete closure type; OCaml predicates have transparent function types like int -> bool visible to the type checker.
  • Dynamic dispatch cost: Rust's Vec<Box<dyn Fn(&T) -> bool>> allocates each predicate on the heap; OCaml's list of predicates also heap-allocates but through GC-managed closures.
  • Ownership of captured state: Rust predicates capturing data must use move; OCaml closures capture by reference to the GC heap automatically.
  • Type genericity: Rust's pred_and<T, P1, P2> works for any T via generics; OCaml's pred_and is polymorphic via HM inference with no explicit type parameters.
  • OCaml Approach

    OCaml predicates are plain functions 'a -> bool. Composition is idiomatic with higher-order functions and no special combinators are needed — the language's native &&/|| can be lifted:

    let pred_and p1 p2 x = p1 x && p2 x
    let pred_or  p1 p2 x = p1 x || p2 x
    let pred_not p x = not (p x)
    let all_of preds x = List.for_all (fun p -> p x) preds
    

    Full Source

    #![allow(clippy::all)]
    //! Predicate Functions Pattern
    //!
    //! Composable predicates: and, or, not, all_of, any_of.
    
    /// Combine two predicates with AND.
    pub fn pred_and<T, P1, P2>(p1: P1, p2: P2) -> impl Fn(&T) -> bool
    where
        P1: Fn(&T) -> bool,
        P2: Fn(&T) -> bool,
    {
        move |x| p1(x) && p2(x)
    }
    
    /// Combine two predicates with OR.
    pub fn pred_or<T, P1, P2>(p1: P1, p2: P2) -> impl Fn(&T) -> bool
    where
        P1: Fn(&T) -> bool,
        P2: Fn(&T) -> bool,
    {
        move |x| p1(x) || p2(x)
    }
    
    /// Negate a predicate.
    pub fn pred_not<T, P>(p: P) -> impl Fn(&T) -> bool
    where
        P: Fn(&T) -> bool,
    {
        move |x| !p(x)
    }
    
    /// All predicates must be true.
    pub fn all_of<T>(preds: Vec<Box<dyn Fn(&T) -> bool>>) -> impl Fn(&T) -> bool {
        move |x| preds.iter().all(|p| p(x))
    }
    
    /// Any predicate must be true.
    pub fn any_of<T>(preds: Vec<Box<dyn Fn(&T) -> bool>>) -> impl Fn(&T) -> bool {
        move |x| preds.iter().any(|p| p(x))
    }
    
    /// Common numeric predicates.
    pub fn is_positive() -> impl Fn(&i32) -> bool {
        |&x| x > 0
    }
    
    pub fn is_even() -> impl Fn(&i32) -> bool {
        |&x| x % 2 == 0
    }
    
    pub fn is_in_range(lo: i32, hi: i32) -> impl Fn(&i32) -> bool {
        move |&x| x >= lo && x <= hi
    }
    
    /// String predicates for &String references.
    pub fn starts_with_str(prefix: String) -> impl Fn(&String) -> bool {
        move |s: &String| s.starts_with(&prefix)
    }
    
    pub fn has_length_str(len: usize) -> impl Fn(&String) -> bool {
        move |s: &String| s.len() == len
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_pred_and() {
            let is_positive_even = pred_and(is_positive(), is_even());
            assert!(is_positive_even(&4));
            assert!(!is_positive_even(&3));
            assert!(!is_positive_even(&-2));
        }
    
        #[test]
        fn test_pred_or() {
            let zero_or_positive = pred_or(|&x: &i32| x == 0, is_positive());
            assert!(zero_or_positive(&0));
            assert!(zero_or_positive(&5));
            assert!(!zero_or_positive(&-1));
        }
    
        #[test]
        fn test_pred_not() {
            let is_odd = pred_not(is_even());
            assert!(is_odd(&3));
            assert!(!is_odd(&4));
        }
    
        #[test]
        fn test_is_in_range() {
            let in_range = is_in_range(1, 10);
            assert!(in_range(&5));
            assert!(in_range(&1));
            assert!(in_range(&10));
            assert!(!in_range(&0));
            assert!(!in_range(&11));
        }
    
        #[test]
        fn test_all_of() {
            let preds: Vec<Box<dyn Fn(&i32) -> bool>> = vec![
                Box::new(is_positive()),
                Box::new(is_even()),
                Box::new(is_in_range(1, 100)),
            ];
            let check = all_of(preds);
            assert!(check(&4));
            assert!(!check(&101));
            assert!(!check(&-2));
        }
    
        #[test]
        fn test_any_of() {
            let preds: Vec<Box<dyn Fn(&i32) -> bool>> = vec![
                Box::new(|&x| x == 0),
                Box::new(|&x| x == 42),
                Box::new(|&x| x == 100),
            ];
            let check = any_of(preds);
            assert!(check(&42));
            assert!(!check(&50));
        }
    
        #[test]
        fn test_string_predicates() {
            let check = pred_and(starts_with_str("hello".into()), has_length_str(11));
            assert!(check(&"hello world".to_string()));
            assert!(!check(&"hello".to_string()));
            assert!(!check(&"hi there!!!".to_string()));
        }
    
        #[test]
        fn test_filter_with_predicate() {
            let is_valid = pred_and(is_positive(), is_even());
            let nums = vec![-2, -1, 0, 1, 2, 3, 4, 5, 6];
            let result: Vec<i32> = nums.into_iter().filter(|x| is_valid(x)).collect();
            assert_eq!(result, vec![2, 4, 6]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_pred_and() {
            let is_positive_even = pred_and(is_positive(), is_even());
            assert!(is_positive_even(&4));
            assert!(!is_positive_even(&3));
            assert!(!is_positive_even(&-2));
        }
    
        #[test]
        fn test_pred_or() {
            let zero_or_positive = pred_or(|&x: &i32| x == 0, is_positive());
            assert!(zero_or_positive(&0));
            assert!(zero_or_positive(&5));
            assert!(!zero_or_positive(&-1));
        }
    
        #[test]
        fn test_pred_not() {
            let is_odd = pred_not(is_even());
            assert!(is_odd(&3));
            assert!(!is_odd(&4));
        }
    
        #[test]
        fn test_is_in_range() {
            let in_range = is_in_range(1, 10);
            assert!(in_range(&5));
            assert!(in_range(&1));
            assert!(in_range(&10));
            assert!(!in_range(&0));
            assert!(!in_range(&11));
        }
    
        #[test]
        fn test_all_of() {
            let preds: Vec<Box<dyn Fn(&i32) -> bool>> = vec![
                Box::new(is_positive()),
                Box::new(is_even()),
                Box::new(is_in_range(1, 100)),
            ];
            let check = all_of(preds);
            assert!(check(&4));
            assert!(!check(&101));
            assert!(!check(&-2));
        }
    
        #[test]
        fn test_any_of() {
            let preds: Vec<Box<dyn Fn(&i32) -> bool>> = vec![
                Box::new(|&x| x == 0),
                Box::new(|&x| x == 42),
                Box::new(|&x| x == 100),
            ];
            let check = any_of(preds);
            assert!(check(&42));
            assert!(!check(&50));
        }
    
        #[test]
        fn test_string_predicates() {
            let check = pred_and(starts_with_str("hello".into()), has_length_str(11));
            assert!(check(&"hello world".to_string()));
            assert!(!check(&"hello".to_string()));
            assert!(!check(&"hi there!!!".to_string()));
        }
    
        #[test]
        fn test_filter_with_predicate() {
            let is_valid = pred_and(is_positive(), is_even());
            let nums = vec![-2, -1, 0, 1, 2, 3, 4, 5, 6];
            let result: Vec<i32> = nums.into_iter().filter(|x| is_valid(x)).collect();
            assert_eq!(result, vec![2, 4, 6]);
        }
    }

    Deep Comparison

    OCaml vs Rust: Predicate Composition

    OCaml

    let pred_and p1 p2 x = p1 x && p2 x
    let pred_or p1 p2 x = p1 x || p2 x
    let pred_not p x = not (p x)
    
    let is_positive x = x > 0
    let is_even x = x mod 2 = 0
    let is_positive_even = pred_and is_positive is_even
    

    Rust

    pub fn pred_and<T, P1, P2>(p1: P1, p2: P2) -> impl Fn(&T) -> bool
    where P1: Fn(&T) -> bool, P2: Fn(&T) -> bool {
        move |x| p1(x) && p2(x)
    }
    
    let is_positive_even = pred_and(is_positive(), is_even());
    

    Key Differences

  • OCaml: Simple function composition
  • Rust: Generic over predicate types with trait bounds
  • Both: Build complex predicates from simple ones
  • Rust: Closures need explicit lifetime/move handling
  • Both integrate with filter operations
  • Exercises

  • String predicate library: Build is_non_empty(), has_prefix(prefix: String), and has_length_between(min, max) as predicate factories and compose them to validate email-like strings.
  • Predicate from regex: Implement matches_pattern(pat: &str) -> impl Fn(&str) -> bool using std::str::contains or a simple substring check, and compose it with pred_not to reject certain strings.
  • Weighted all_of: Extend all_of to all_of_weighted(preds: Vec<(Box<dyn Fn(&T) -> bool>, f64)>) that returns the sum of weights of satisfied predicates, enabling scoring rather than binary pass/fail.
  • Open Source Repos