Predicate Functions Pattern
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
impl Fn(&T) -> bool return typespred_and, pred_or, and pred_not build new predicates from existing onesall_of and any_of generalize to dynamic lists of predicatesIterator::filter for expressive queriesCode 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
impl Fn(&T) -> bool hides the concrete closure type; OCaml predicates have transparent function types like int -> bool visible to the type checker.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.move; OCaml closures capture by reference to the GC heap automatically.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]);
}
}#[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
Exercises
is_non_empty(), has_prefix(prefix: String), and has_length_between(min, max) as predicate factories and compose them to validate email-like strings.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.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.