ExamplesBy LevelBy TopicLearning Paths
078 Intermediate

078 — Where Clauses

Functional Programming

Tutorial

The Problem

Express complex trait bounds on generic functions and types using Rust's where clause syntax. Implement print_if_equal, zip_with, sum_items, dot_product, and display_collection — each requiring multiple or compound constraints — and compare with OCaml's module functor approach to constraining polymorphic code.

🎯 Learning Outcomes

  • • Write where clauses to separate complex bounds from the function signature
  • • Combine multiple trait bounds on a single type parameter (T: Display + PartialEq)
  • • Apply bounds to associated types of iterator (I::Item: Add + Default)
  • • Use IntoIterator with I::Item: Display for generic collection printing
  • • Understand when where improves readability over inline bound syntax
  • • Map OCaml functor module signatures to Rust where constraints
  • Code Example

    #![allow(clippy::all)]
    // 078: Where Clauses
    // Complex trait bounds using where syntax
    
    use std::fmt::Display;
    use std::ops::{Add, Mul};
    
    // Approach 1: Where clause for readability
    fn print_if_equal<T>(a: &T, b: &T) -> String
    where
        T: Display + PartialEq,
    {
        if a == b {
            format!("{} == {}", a, b)
        } else {
            format!("{} != {}", a, b)
        }
    }
    
    // Approach 2: Multiple type params with where
    fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
    where
        A: Clone,
        B: Clone,
        F: Fn(A, B) -> C,
    {
        a.iter()
            .cloned()
            .zip(b.iter().cloned())
            .map(|(x, y)| f(x, y))
            .collect()
    }
    
    // Approach 3: Associated type bounds
    fn sum_items<I>(iter: I) -> I::Item
    where
        I: Iterator,
        I::Item: Add<Output = I::Item> + Default,
    {
        iter.fold(I::Item::default(), |acc, x| acc + x)
    }
    
    fn dot_product<T>(a: &[T], b: &[T]) -> T
    where
        T: Add<Output = T> + Mul<Output = T> + Default + Copy,
    {
        a.iter()
            .zip(b.iter())
            .fold(T::default(), |acc, (&x, &y)| acc + x * y)
    }
    
    // Complex: display collection of displayable items
    fn display_collection<I>(iter: I) -> String
    where
        I: IntoIterator,
        I::Item: Display,
    {
        let items: Vec<String> = iter.into_iter().map(|x| format!("{}", x)).collect();
        format!("[{}]", items.join(", "))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_print_if_equal() {
            assert_eq!(print_if_equal(&5, &5), "5 == 5");
            assert_eq!(print_if_equal(&3, &4), "3 != 4");
        }
    
        #[test]
        fn test_zip_with() {
            assert_eq!(
                zip_with(&[1, 2, 3], &[4, 5, 6], |a, b| a + b),
                vec![5, 7, 9]
            );
            assert_eq!(zip_with(&[1, 2], &[3, 4], |a, b| a * b), vec![3, 8]);
        }
    
        #[test]
        fn test_sum_items() {
            assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
        }
    
        #[test]
        fn test_dot_product() {
            assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
        }
    
        #[test]
        fn test_display_collection() {
            assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
        }
    }

    Key Differences

    AspectRustOCaml
    Syntaxwhere T: Trait1 + Trait2module type SIG = sig … end
    ScopePer function/impl blockModule-level functor
    Associated type boundsI::Item: Trait in wherewith type item = t refinement
    Constraint composition+ on trait boundsinclude in module types
    MonomorphizationYes, at compile timeFunctors instantiated at application
    Runtime costNoneNone

    where syntax is purely cosmetic in most cases — it does not change semantics versus inline bounds. The exception is associated type constraints, which require where. OCaml functors produce named modules, making them first-class values; Rust generic functions are erased into monomorphized copies.

    OCaml Approach

    OCaml expresses constraints via module signatures. A functor MathOps(S : SUMMABLE) requires the input module to provide zero, add, and to_string. More complex constraints combine signatures: module type RING = sig include SUMMABLE include MULTIPLIABLE end. Concrete modules like IntSum and FloatSum are produced by applying the functor to struct-style anonymous modules. The type system ensures constraints are satisfied at functor application, not at call sites — equivalent to Rust's monomorphization.

    Full Source

    #![allow(clippy::all)]
    // 078: Where Clauses
    // Complex trait bounds using where syntax
    
    use std::fmt::Display;
    use std::ops::{Add, Mul};
    
    // Approach 1: Where clause for readability
    fn print_if_equal<T>(a: &T, b: &T) -> String
    where
        T: Display + PartialEq,
    {
        if a == b {
            format!("{} == {}", a, b)
        } else {
            format!("{} != {}", a, b)
        }
    }
    
    // Approach 2: Multiple type params with where
    fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
    where
        A: Clone,
        B: Clone,
        F: Fn(A, B) -> C,
    {
        a.iter()
            .cloned()
            .zip(b.iter().cloned())
            .map(|(x, y)| f(x, y))
            .collect()
    }
    
    // Approach 3: Associated type bounds
    fn sum_items<I>(iter: I) -> I::Item
    where
        I: Iterator,
        I::Item: Add<Output = I::Item> + Default,
    {
        iter.fold(I::Item::default(), |acc, x| acc + x)
    }
    
    fn dot_product<T>(a: &[T], b: &[T]) -> T
    where
        T: Add<Output = T> + Mul<Output = T> + Default + Copy,
    {
        a.iter()
            .zip(b.iter())
            .fold(T::default(), |acc, (&x, &y)| acc + x * y)
    }
    
    // Complex: display collection of displayable items
    fn display_collection<I>(iter: I) -> String
    where
        I: IntoIterator,
        I::Item: Display,
    {
        let items: Vec<String> = iter.into_iter().map(|x| format!("{}", x)).collect();
        format!("[{}]", items.join(", "))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_print_if_equal() {
            assert_eq!(print_if_equal(&5, &5), "5 == 5");
            assert_eq!(print_if_equal(&3, &4), "3 != 4");
        }
    
        #[test]
        fn test_zip_with() {
            assert_eq!(
                zip_with(&[1, 2, 3], &[4, 5, 6], |a, b| a + b),
                vec![5, 7, 9]
            );
            assert_eq!(zip_with(&[1, 2], &[3, 4], |a, b| a * b), vec![3, 8]);
        }
    
        #[test]
        fn test_sum_items() {
            assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
        }
    
        #[test]
        fn test_dot_product() {
            assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
        }
    
        #[test]
        fn test_display_collection() {
            assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_print_if_equal() {
            assert_eq!(print_if_equal(&5, &5), "5 == 5");
            assert_eq!(print_if_equal(&3, &4), "3 != 4");
        }
    
        #[test]
        fn test_zip_with() {
            assert_eq!(
                zip_with(&[1, 2, 3], &[4, 5, 6], |a, b| a + b),
                vec![5, 7, 9]
            );
            assert_eq!(zip_with(&[1, 2], &[3, 4], |a, b| a * b), vec![3, 8]);
        }
    
        #[test]
        fn test_sum_items() {
            assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
        }
    
        #[test]
        fn test_dot_product() {
            assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
        }
    
        #[test]
        fn test_display_collection() {
            assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
        }
    }

    Deep Comparison

    Core Insight

    Where clauses move trait bounds after the function signature for clarity. Essential when bounds involve associated types, multiple type parameters, or complex relationships.

    OCaml Approach

  • • No direct equivalent — module functors handle complex constraints
  • • Type constraints in module signatures
  • Rust Approach

  • where T: Trait, U: Trait after signature
  • • Required for associated type bounds: where I::Item: Display
  • • Cleaner than inline bounds for complex cases
  • Comparison Table

    FeatureOCamlRust
    Simple boundN/A<T: Display>
    Complex boundFunctor signaturewhere T: A + B, U: C
    Associated typeModule typewhere I::Item: Display

    Exercises

  • Add a min_max function with signature fn min_max<T>(slice: &[T]) -> Option<(&T, &T)> using a where clause requiring T: PartialOrd.
  • Write map_collect<I, F, B>(iter: I, f: F) -> Vec<B> using where to express the iterator and closure bounds separately.
  • Implement a Printable trait with a print method, then use where T: Printable + Clone in a function that clones and prints each element of a slice.
  • Create a functor in OCaml called Sorted(C : COMPARABLE) and implement a sorted insertion function. Compare the constraint surface area with the Rust where equivalent.
  • Explore the difference between fn f<T: A + B>() and fn f<T>() where T: A + B. Verify both compile identically with cargo expand or by checking the generated MIR.
  • Open Source Repos