ExamplesBy LevelBy TopicLearning Paths
872 Intermediate

872-where-clauses — Where Clauses

Functional Programming

Tutorial

The Problem

When a generic function involves multiple type parameters, each with several bounds, the inline bound syntax <T: A + B, U: C + D, F: Fn(T) -> U> becomes unwieldy. Rust's where clause separates the type parameter list from the constraints, moving them to a dedicated block after the function signature. This improves readability for complex higher-order functions, especially those accepting multiple function parameters. OCaml achieves the same clarity through structural typing and module signatures, which do not require listing constraints inline.

🎯 Learning Outcomes

  • • Write complex multi-parameter generic functions using where clauses
  • • Understand when where clauses are required (trait bounds on associated types)
  • • Compare inline bounds vs where clause style for readability
  • • Implement higher-order generic functions with separate transform and combine parameters
  • • Recognize how where clauses scale to real-world generic combinators
  • Code Example

    fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
        slice.iter().reduce(|a, b| if a >= b { a } else { b })
    }

    Key Differences

  • Syntactic placement: Rust where clauses follow the function signature; OCaml constraints appear in module type signatures used as functor parameters.
  • Required for associated types: Rust where is mandatory when constraining associated types (e.g., where T::Item: Display); OCaml handles this via module type refinement.
  • Readability threshold: Rust style guides recommend where when there are more than two bounds or multiple type parameters; OCaml has no equivalent guideline.
  • No runtime cost: Both approaches are purely compile-time mechanisms with no runtime overhead.
  • OCaml Approach

    OCaml achieves constraint clarity through structural module types. A functor MakeProcessor(M: Mappable) separates the constraint (the module signature) from the implementation. For plain functions, OCaml uses implicit structural polymorphism or explicit function parameters like ~transform ~combine ~init. Since OCaml doesn't have inline bound syntax, all constraints are implicitly structural — there is no equivalent to the where clause syntactic distinction.

    Full Source

    #![allow(clippy::all)]
    // Example 078: Where Clauses
    // Complex where clauses vs inline bounds
    
    use std::fmt::{Debug, Display};
    use std::ops::{Add, Mul};
    
    // === Approach 1: Inline bounds (simple cases) ===
    fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
        slice.iter().reduce(|a, b| if a >= b { a } else { b })
    }
    
    // === Approach 2: Where clauses (complex constraints) ===
    fn transform_and_combine<T, U, A, F, G>(items: &[T], transform: F, combine: G, init: A) -> A
    where
        F: Fn(&T) -> U,
        G: Fn(A, U) -> A,
    {
        items.iter().fold(init, |acc, x| combine(acc, transform(x)))
    }
    
    fn filter_map_fold<T, U, A, P, F, G>(items: &[T], pred: P, transform: F, combine: G, init: A) -> A
    where
        P: Fn(&T) -> bool,
        F: Fn(&T) -> U,
        G: Fn(A, U) -> A,
    {
        items.iter().fold(init, |acc, x| {
            if pred(x) {
                combine(acc, transform(x))
            } else {
                acc
            }
        })
    }
    
    // Where clause shines with multiple related bounds
    fn sorted_summary<T>(items: &mut [T]) -> String
    where
        T: Ord + Display,
    {
        items.sort();
        items
            .iter()
            .map(|x| x.to_string())
            .collect::<Vec<_>>()
            .join(", ")
    }
    
    // === Approach 3: Complex multi-type where clauses ===
    fn bounded_transform<T, F>(items: &[T], transform: F, lo: T, hi: T) -> Vec<T>
    where
        T: PartialOrd + Clone,
        F: Fn(&T) -> T,
    {
        items
            .iter()
            .map(|x| {
                let y = transform(x);
                if y < lo {
                    lo.clone()
                } else if y > hi {
                    hi.clone()
                } else {
                    y
                }
            })
            .collect()
    }
    
    // Return type bounds in where clause
    fn numeric_summary<T>(a: T, b: T) -> String
    where
        T: Add<Output = T> + Mul<Output = T> + Display + Copy,
    {
        let sum = a + b;
        let product = a * b;
        format!("sum={}, product={}", sum, product)
    }
    
    // Where clause with lifetime + trait bounds
    fn longest_display<'a, T>(a: &'a T, b: &'a T) -> String
    where
        T: Display + PartialOrd,
    {
        if a >= b {
            format!("{}", a)
        } else {
            format!("{}", b)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_transform_and_combine() {
            let r = transform_and_combine(&[1, 2, 3, 4], |x| x * x, |a, b| a + b, 0);
            assert_eq!(r, 30);
        }
    
        #[test]
        fn test_filter_map_fold() {
            let r = filter_map_fold(
                &[1, 2, 3, 4, 5, 6],
                |x| x % 2 == 0,
                |x| x * x,
                |a, b| a + b,
                0,
            );
            assert_eq!(r, 56); // 4 + 16 + 36
        }
    
        #[test]
        fn test_sorted_summary() {
            let mut v = vec![3, 1, 4, 1, 5];
            assert_eq!(sorted_summary(&mut v), "1, 1, 3, 4, 5");
        }
    
        #[test]
        fn test_bounded_transform() {
            let r = bounded_transform(&[1, 2, 3, 4, 5], |x| x * 3, 0, 10);
            assert_eq!(r, vec![3, 6, 9, 10, 10]);
        }
    
        #[test]
        fn test_numeric_summary() {
            assert_eq!(numeric_summary(3, 4), "sum=7, product=12");
        }
    
        #[test]
        fn test_longest_display() {
            assert_eq!(longest_display(&10, &20), "20");
            assert_eq!(longest_display(&"zebra", &"apple"), "zebra");
        }
    
        #[test]
        fn test_empty_slice() {
            let r = transform_and_combine::<i32, i32, i32, _, _>(&[], |x| x * x, |a, b| a + b, 0);
            assert_eq!(r, 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_transform_and_combine() {
            let r = transform_and_combine(&[1, 2, 3, 4], |x| x * x, |a, b| a + b, 0);
            assert_eq!(r, 30);
        }
    
        #[test]
        fn test_filter_map_fold() {
            let r = filter_map_fold(
                &[1, 2, 3, 4, 5, 6],
                |x| x % 2 == 0,
                |x| x * x,
                |a, b| a + b,
                0,
            );
            assert_eq!(r, 56); // 4 + 16 + 36
        }
    
        #[test]
        fn test_sorted_summary() {
            let mut v = vec![3, 1, 4, 1, 5];
            assert_eq!(sorted_summary(&mut v), "1, 1, 3, 4, 5");
        }
    
        #[test]
        fn test_bounded_transform() {
            let r = bounded_transform(&[1, 2, 3, 4, 5], |x| x * 3, 0, 10);
            assert_eq!(r, vec![3, 6, 9, 10, 10]);
        }
    
        #[test]
        fn test_numeric_summary() {
            assert_eq!(numeric_summary(3, 4), "sum=7, product=12");
        }
    
        #[test]
        fn test_longest_display() {
            assert_eq!(longest_display(&10, &20), "20");
            assert_eq!(longest_display(&"zebra", &"apple"), "zebra");
        }
    
        #[test]
        fn test_empty_slice() {
            let r = transform_and_combine::<i32, i32, i32, _, _>(&[], |x| x * x, |a, b| a + b, 0);
            assert_eq!(r, 0);
        }
    }

    Deep Comparison

    Comparison: Where Clauses

    Inline vs Where Clause

    Rust — Inline bounds (simple):

    fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
        slice.iter().reduce(|a, b| if a >= b { a } else { b })
    }
    

    Rust — Where clause (complex):

    fn transform_and_combine<T, U, A, F, G>(items: &[T], transform: F, combine: G, init: A) -> A
    where
        F: Fn(&T) -> U,
        G: Fn(A, U) -> A,
    {
        items.iter().fold(init, |acc, x| combine(acc, transform(x)))
    }
    

    OCaml Equivalent — No Explicit Constraints

    OCaml:

    let transform_and_combine ~transform ~combine ~init items =
      List.fold_left (fun acc x -> combine acc (transform x)) init items
    (* Types are fully inferred, no constraints written *)
    

    Rust:

    fn transform_and_combine<T, U, A, F, G>(items: &[T], transform: F, combine: G, init: A) -> A
    where F: Fn(&T) -> U, G: Fn(A, U) -> A,
    { /* ... */ }
    

    Multiple Related Bounds

    OCaml:

    let sorted_summary items to_str =
      let sorted = List.sort compare items in
      String.concat ", " (List.map to_str sorted)
    

    Rust:

    fn sorted_summary<T>(items: &mut [T]) -> String
    where
        T: Ord + Display,
    {
        items.sort();
        items.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", ")
    }
    

    Exercises

  • Write a merge_maps<K, V, F> function using a where clause where K: Ord + Clone, V: Clone, and F: Fn(V, V) -> V merges duplicate keys.
  • Implement a generic pipeline<T, F1, F2, F3> that chains three transformations, using a where clause to bound each Fi: Fn(T) -> T.
  • Rewrite transform_and_combine using inline bounds and compare readability — document which style you prefer and why.
  • Open Source Repos