ExamplesBy LevelBy TopicLearning Paths
871 Intermediate

871-generic-bounds — Generic Bounds

Functional Programming

Tutorial

The Problem

Generic programming allows a single function to operate over many types, but only those types that satisfy certain contracts. In C++, this was implicit (duck typing) leading to cryptic template errors. Rust and Haskell require explicit constraints: T: PartialOrd + Display means "any type T that supports comparison and formatting." OCaml uses module signatures and functors to express similar constraints. Generic bounds are the foundation of type-safe abstractions: they enable writing find_max once that works for integers, floats, strings, or any custom comparable type.

🎯 Learning Outcomes

  • • Express single and multiple trait bounds with the <T: Trait1 + Trait2> syntax
  • • Use the Display and PartialOrd standard library traits as common bounds
  • • Implement a custom trait that inherits from another trait as a supertrait
  • • Understand how bounds enable generic algorithms without sacrificing type safety
  • • Compare Rust trait bounds with OCaml's module type constraints
  • Code Example

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

    Key Differences

  • Resolution time: Rust bounds are resolved at compile time via monomorphization; OCaml's polymorphic functions use runtime representation with the universal compare.
  • Supertrait inheritance: Rust trait Summarize: Display enforces Display must also be implemented; OCaml module types compose by inclusion in the signature.
  • Multiple bounds: Rust uses + syntax (T: A + B); OCaml composes module types with include or functor composition.
  • Error messages: Rust bound violations produce targeted errors ("T doesn't implement Display"); OCaml module mismatches can produce verbose signature errors.
  • OCaml Approach

    OCaml uses module types as the constraint mechanism: module type Comparable = sig type t; val compare: t -> t -> int end. A functor MakeProcessor(M: Comparable) is parameterized over any module satisfying the signature. For simple cases, OCaml uses its built-in polymorphic compare function (which works on any type), avoiding explicit constraints. The explicit comparison function parameter pattern find_max_by (cmp: 'a -> 'a -> int) mirrors Rust's trait bound but passed as a value rather than resolved at compile time.

    Full Source

    #![allow(clippy::all)]
    // Example 077: Generic Bounds
    // OCaml type constraints → Rust <T: Trait> bounds
    
    use std::fmt::Display;
    
    // === Approach 1: Single trait bound ===
    fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
        slice.iter().reduce(|a, b| if a >= b { a } else { b })
    }
    
    fn find_min<T: PartialOrd>(slice: &[T]) -> Option<&T> {
        slice.iter().reduce(|a, b| if a <= b { a } else { b })
    }
    
    // === Approach 2: Multiple trait bounds ===
    fn print_max<T: PartialOrd + Display>(slice: &[T]) -> Option<String> {
        find_max(slice).map(|v| format!("Max: {}", v))
    }
    
    fn clamp<T: PartialOrd>(value: T, lo: T, hi: T) -> T {
        if value < lo {
            lo
        } else if value > hi {
            hi
        } else {
            value
        }
    }
    
    // === Approach 3: Custom trait with bounds ===
    trait Summarize: Display {
        fn summary(&self) -> String;
    }
    
    struct Stats {
        name: String,
        value: f64,
    }
    
    impl Display for Stats {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}={:.2}", self.name, self.value)
        }
    }
    
    impl Summarize for Stats {
        fn summary(&self) -> String {
            format!("[{}]", self)
        }
    }
    
    fn print_summaries<T: Summarize>(items: &[T]) -> String {
        items
            .iter()
            .map(|i| i.summary())
            .collect::<Vec<_>>()
            .join(", ")
    }
    
    // Generic pair operations with bounds
    fn pair_map<T, U, F: Fn(T) -> U>(pair: (T, T), f: F) -> (U, U) {
        (f(pair.0), f(pair.1))
    }
    
    fn pair_fold<T, A, F: Fn(A, T) -> A>(init: A, pair: (T, T), f: F) -> A {
        let acc = f(init, pair.0);
        f(acc, pair.1)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_find_max() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5, 9]), Some(&9));
            assert_eq!(find_max::<i32>(&[]), None);
            assert_eq!(find_max(&[42]), Some(&42));
        }
    
        #[test]
        fn test_find_min() {
            assert_eq!(find_min(&[3, 1, 4, 1, 5, 9]), Some(&1));
            assert_eq!(find_min::<i32>(&[]), None);
        }
    
        #[test]
        fn test_clamp() {
            assert_eq!(clamp(15, 0, 10), 10);
            assert_eq!(clamp(-5, 0, 10), 0);
            assert_eq!(clamp(5, 0, 10), 5);
            assert_eq!(clamp(1.5, 0.0, 1.0), 1.0);
        }
    
        #[test]
        fn test_print_max() {
            assert_eq!(print_max(&[1, 2, 3]), Some("Max: 3".to_string()));
            assert_eq!(print_max::<i32>(&[]), None);
        }
    
        #[test]
        fn test_pair_map() {
            assert_eq!(pair_map((3, 4), |x| x * 2), (6, 8));
            assert_eq!(
                pair_map((1.0, 2.0), |x: f64| x.sqrt()),
                (1.0, std::f64::consts::SQRT_2)
            );
        }
    
        #[test]
        fn test_pair_fold() {
            assert_eq!(pair_fold(0, (3, 4), |acc, x| acc + x), 7);
        }
    
        #[test]
        fn test_summarize() {
            let s = Stats {
                name: "x".into(),
                value: 1.0,
            };
            assert_eq!(s.summary(), "[x=1.00]");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_find_max() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5, 9]), Some(&9));
            assert_eq!(find_max::<i32>(&[]), None);
            assert_eq!(find_max(&[42]), Some(&42));
        }
    
        #[test]
        fn test_find_min() {
            assert_eq!(find_min(&[3, 1, 4, 1, 5, 9]), Some(&1));
            assert_eq!(find_min::<i32>(&[]), None);
        }
    
        #[test]
        fn test_clamp() {
            assert_eq!(clamp(15, 0, 10), 10);
            assert_eq!(clamp(-5, 0, 10), 0);
            assert_eq!(clamp(5, 0, 10), 5);
            assert_eq!(clamp(1.5, 0.0, 1.0), 1.0);
        }
    
        #[test]
        fn test_print_max() {
            assert_eq!(print_max(&[1, 2, 3]), Some("Max: 3".to_string()));
            assert_eq!(print_max::<i32>(&[]), None);
        }
    
        #[test]
        fn test_pair_map() {
            assert_eq!(pair_map((3, 4), |x| x * 2), (6, 8));
            assert_eq!(
                pair_map((1.0, 2.0), |x: f64| x.sqrt()),
                (1.0, std::f64::consts::SQRT_2)
            );
        }
    
        #[test]
        fn test_pair_fold() {
            assert_eq!(pair_fold(0, (3, 4), |acc, x| acc + x), 7);
        }
    
        #[test]
        fn test_summarize() {
            let s = Stats {
                name: "x".into(),
                value: 1.0,
            };
            assert_eq!(s.summary(), "[x=1.00]");
        }
    }

    Deep Comparison

    Comparison: Generic Bounds

    Single Bound

    OCaml — Implicit polymorphism:

    let find_max lst =
      match lst with
      | [] -> None
      | x :: rest -> Some (List.fold_left max x rest)
    

    Rust — Explicit trait bound:

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

    Multiple Bounds

    OCaml — Structural (no syntax needed):

    let print_max lst =
      match find_max lst with
      | None -> "empty"
      | Some x -> Printf.sprintf "Max: %s" (string_of_int x)
    

    **Rust — Combined with +:**

    fn print_max<T: PartialOrd + Display>(slice: &[T]) -> Option<String> {
        find_max(slice).map(|v| format!("Max: {}", v))
    }
    

    Trait Hierarchy

    OCaml — Module type inclusion:

    module type Printable = sig
      type t
      val to_string : t -> string
    end
    

    Rust — Supertrait:

    trait Summarize: Display {
        fn summary(&self) -> String;
    }
    

    Exercises

  • Add a Bounded trait with min_value() and max_value() associated functions, and implement it for i32 and f64.
  • Write a generic statistics<T: PartialOrd + Clone> function that returns (min, max, first, last) in one pass over a slice.
  • Implement a sorted_unique<T: Ord + Clone> function that deduplicates a sorted slice, requiring both ordering and cloning bounds.
  • Open Source Repos