ExamplesBy LevelBy TopicLearning Paths
393 Intermediate

393: Trait Bounds and Where Clauses

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "393: Trait Bounds and Where Clauses" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Generic Rust code must express what operations a type parameter supports. Key difference from OCaml: 1. **Nominal vs. structural**: Rust bounds are nominal — types must explicitly `impl` the trait; OCaml constraints are structural — any module providing the required functions satisfies the constraint.

Tutorial

The Problem

Generic Rust code must express what operations a type parameter supports. Trait bounds (T: Display + Clone) appear inline in angle brackets for simple cases, but complex functions with many parameters and bounds become illegible. The where clause separates the function signature from its constraints, improving readability when bounds are long, when the same bound applies to multiple parameters, or when bounds involve associated types. Both forms are semantically identical — it is purely an ergonomic choice.

Trait bounds and where clauses are the building blocks of all generic Rust code: every standard library function, every serde derive, every tokio task uses them to express type requirements.

🎯 Learning Outcomes

  • • Understand trait bounds as compile-time contracts specifying what operations T supports
  • • Learn the equivalence between inline bounds (T: A + B) and where clause form
  • • See when where clauses improve readability (multiple parameters, long bounds, associated type bounds)
  • • Understand compound bounds (T: Debug + Clone, lifetime bounds T: 'a)
  • • Learn how bounds interact with lifetimes in longest_with_debug-style functions
  • Code Example

    #![allow(clippy::all)]
    //! Trait Bounds and Where Clauses
    
    use std::fmt::{Debug, Display};
    use std::hash::Hash;
    
    pub fn print_debug<T: Debug>(val: T) {
        println!("{:?}", val);
    }
    pub fn compare_and_display<T: PartialOrd + Display>(a: T, b: T) -> String {
        if a < b {
            format!("{} < {}", a, b)
        } else {
            format!("{} >= {}", a, b)
        }
    }
    
    pub fn complex_function<T, U>(t: T, u: U) -> String
    where
        T: Debug + Clone,
        U: Display + Hash,
    {
        format!("{:?} and {}", t, u)
    }
    
    pub fn longest_with_debug<'a, T>(a: &'a T, b: &'a T) -> &'a T
    where
        T: PartialOrd + Debug,
    {
        if a > b {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compare() {
            assert!(compare_and_display(1, 2).contains("<"));
        }
        #[test]
        fn test_complex() {
            assert!(complex_function(42, "hello").contains("42"));
        }
        #[test]
        fn test_longest() {
            assert_eq!(longest_with_debug(&5, &3), &5);
        }
        #[test]
        fn test_compare_eq() {
            assert!(compare_and_display(2, 2).contains(">="));
        }
    }

    Key Differences

  • Nominal vs. structural: Rust bounds are nominal — types must explicitly impl the trait; OCaml constraints are structural — any module providing the required functions satisfies the constraint.
  • Inference: OCaml infers bounds from usage; Rust requires explicit annotation in function signatures.
  • Readability: Rust's where clause directly mirrors OCaml's functor parameter style; both achieve separation of concerns between signature and constraints.
  • Lifetime bounds: Rust has explicit lifetime bounds (T: 'a); OCaml manages lifetimes through GC and has no lifetime annotations.
  • OCaml Approach

    OCaml expresses constraints through module types in functor signatures: module F (T : sig val compare : 'a -> 'a -> int val to_string : 'a -> string end). Type constraints are structural (based on what operations exist) rather than nominal (based on declared trait impls). OCaml's type inference often eliminates explicit constraints entirely, inferring them from usage.

    Full Source

    #![allow(clippy::all)]
    //! Trait Bounds and Where Clauses
    
    use std::fmt::{Debug, Display};
    use std::hash::Hash;
    
    pub fn print_debug<T: Debug>(val: T) {
        println!("{:?}", val);
    }
    pub fn compare_and_display<T: PartialOrd + Display>(a: T, b: T) -> String {
        if a < b {
            format!("{} < {}", a, b)
        } else {
            format!("{} >= {}", a, b)
        }
    }
    
    pub fn complex_function<T, U>(t: T, u: U) -> String
    where
        T: Debug + Clone,
        U: Display + Hash,
    {
        format!("{:?} and {}", t, u)
    }
    
    pub fn longest_with_debug<'a, T>(a: &'a T, b: &'a T) -> &'a T
    where
        T: PartialOrd + Debug,
    {
        if a > b {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compare() {
            assert!(compare_and_display(1, 2).contains("<"));
        }
        #[test]
        fn test_complex() {
            assert!(complex_function(42, "hello").contains("42"));
        }
        #[test]
        fn test_longest() {
            assert_eq!(longest_with_debug(&5, &3), &5);
        }
        #[test]
        fn test_compare_eq() {
            assert!(compare_and_display(2, 2).contains(">="));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_compare() {
            assert!(compare_and_display(1, 2).contains("<"));
        }
        #[test]
        fn test_complex() {
            assert!(complex_function(42, "hello").contains("42"));
        }
        #[test]
        fn test_longest() {
            assert_eq!(longest_with_debug(&5, &3), &5);
        }
        #[test]
        fn test_compare_eq() {
            assert!(compare_and_display(2, 2).contains(">="));
        }
    }

    Deep Comparison

    OCaml vs Rust: 393-trait-bounds-where

    Exercises

  • Generic max: Write fn max_of_three<T: PartialOrd>(a: T, b: T, c: T) -> T returning the largest value. Then add a Display bound and print the winner with println!.
  • Cache with bounds: Implement struct Cache<K, V> where K: Eq + Hash + Clone and V: Clone. Use where clauses throughout. Add get, insert, and invalidate_all methods.
  • Bound refactoring: Take the three functions in src/lib.rs with inline bounds and convert them all to where clause form. Then take complex_function and convert it back to inline. Discuss which form is clearer for each case in a code comment.
  • Open Source Repos