ExamplesBy LevelBy TopicLearning Paths
077 Intermediate

077 — Generic Bounds

Functional Programming

Tutorial

The Problem

Generic bounds constrain which types a generic function or struct can be used with. fn print_list<T: Display>(items: &[T]) works for any T that implements Display. Without bounds, generic code cannot call any methods on T. With bounds, you unlock the full interface of the trait.

Bounds are Rust's mechanism for "bounded polymorphism" — the type theory concept underlying Java interfaces, Haskell typeclasses, and OCaml module signatures. They enable writing algorithms once that work across many types, with the compiler verifying type safety and monomorphizing for performance.

🎯 Learning Outcomes

  • • Use single trait bounds: T: Display
  • • Use multiple bounds with +: T: Display + Clone
  • • Use bounds for arithmetic: T: std::iter::Sum + Copy
  • • Implement generic functions that call trait methods on their type parameters
  • • Understand the difference between bounds on type parameters and where clauses
  • Code Example

    #![allow(clippy::all)]
    // 077: Generic Bounds
    // Constraining generic types with trait bounds
    
    use std::fmt::Display;
    
    // Approach 1: Single bound
    fn print_item<T: Display>(item: &T) -> String {
        format!("{}", item)
    }
    
    fn print_list<T: Display>(items: &[T]) -> String {
        let strs: Vec<String> = items.iter().map(|x| format!("{}", x)).collect();
        format!("[{}]", strs.join(", "))
    }
    
    // Approach 2: Multiple bounds
    fn print_and_clone<T: Display + Clone>(item: &T) -> (String, T) {
        (format!("{}", item), item.clone())
    }
    
    fn find_max<T: PartialOrd + Clone>(items: &[T]) -> Option<T> {
        items
            .iter()
            .cloned()
            .reduce(|a, b| if a >= b { a } else { b })
    }
    
    // Approach 3: Bounds for arithmetic
    fn sum_items<T: std::iter::Sum + Copy>(items: &[T]) -> T {
        items.iter().copied().sum()
    }
    
    fn contains<T: PartialEq>(items: &[T], target: &T) -> bool {
        items.iter().any(|x| x == target)
    }
    
    // Generic pair with bounds
    fn larger<T: PartialOrd>(a: T, b: T) -> T {
        if a >= b {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_print_item() {
            assert_eq!(print_item(&42), "42");
            assert_eq!(print_item(&"hello"), "hello");
        }
    
        #[test]
        fn test_print_list() {
            assert_eq!(print_list(&[1, 2, 3]), "[1, 2, 3]");
        }
    
        #[test]
        fn test_print_and_clone() {
            let (s, v) = print_and_clone(&42);
            assert_eq!(s, "42");
            assert_eq!(v, 42);
        }
    
        #[test]
        fn test_find_max() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5]), Some(5));
            assert_eq!(find_max::<i32>(&[]), None);
        }
    
        #[test]
        fn test_contains() {
            assert!(contains(&[1, 2, 3], &2));
            assert!(!contains(&[1, 2, 3], &4));
        }
    
        #[test]
        fn test_larger() {
            assert_eq!(larger(10, 20), 20);
            assert_eq!(larger("z", "a"), "z");
        }
    }

    Key Differences

  • Implicit vs explicit: Rust resolves trait implementations implicitly — T: Display finds the implementation automatically. OCaml passes trait dictionaries (module arguments) explicitly in the traditional style.
  • Multiple bounds: Rust's T: A + B + C is compact. OCaml's functor approach requires a module combining all required interfaces.
  • **where clause**: where T: Display + Clone is equivalent to the inline bound but more readable for complex constraints. Both compile to the same code.
  • Monomorphization: Rust monomorphizes generic functions per concrete type — print_item::<i32> and print_item::<String> are separate compiled functions. OCaml uses boxing for polymorphism (no monomorphization by default).
  • OCaml Approach

    OCaml passes trait "dictionaries" explicitly as module or function arguments:

    (* Explicit dictionary passing — the function receives the "show" implementation *)
    let print_item (type a) (show : a -> string) (x : a) = print_string (show x)
    
    (* Module functor approach — equivalent to Rust's generic bounds *)
    module type ORDERED = sig
      type t
      val compare : t -> t -> int
    end
    
    module FindMax(O : ORDERED) = struct
      let find_max lst =
        List.fold_left
          (fun acc x -> if O.compare x acc > 0 then x else acc)
          (List.hd lst) (List.tl lst)
    end
    

    OCaml's modular implicits (a research extension) would make this automatic, similar to Rust's trait resolution.

    Full Source

    #![allow(clippy::all)]
    // 077: Generic Bounds
    // Constraining generic types with trait bounds
    
    use std::fmt::Display;
    
    // Approach 1: Single bound
    fn print_item<T: Display>(item: &T) -> String {
        format!("{}", item)
    }
    
    fn print_list<T: Display>(items: &[T]) -> String {
        let strs: Vec<String> = items.iter().map(|x| format!("{}", x)).collect();
        format!("[{}]", strs.join(", "))
    }
    
    // Approach 2: Multiple bounds
    fn print_and_clone<T: Display + Clone>(item: &T) -> (String, T) {
        (format!("{}", item), item.clone())
    }
    
    fn find_max<T: PartialOrd + Clone>(items: &[T]) -> Option<T> {
        items
            .iter()
            .cloned()
            .reduce(|a, b| if a >= b { a } else { b })
    }
    
    // Approach 3: Bounds for arithmetic
    fn sum_items<T: std::iter::Sum + Copy>(items: &[T]) -> T {
        items.iter().copied().sum()
    }
    
    fn contains<T: PartialEq>(items: &[T], target: &T) -> bool {
        items.iter().any(|x| x == target)
    }
    
    // Generic pair with bounds
    fn larger<T: PartialOrd>(a: T, b: T) -> T {
        if a >= b {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_print_item() {
            assert_eq!(print_item(&42), "42");
            assert_eq!(print_item(&"hello"), "hello");
        }
    
        #[test]
        fn test_print_list() {
            assert_eq!(print_list(&[1, 2, 3]), "[1, 2, 3]");
        }
    
        #[test]
        fn test_print_and_clone() {
            let (s, v) = print_and_clone(&42);
            assert_eq!(s, "42");
            assert_eq!(v, 42);
        }
    
        #[test]
        fn test_find_max() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5]), Some(5));
            assert_eq!(find_max::<i32>(&[]), None);
        }
    
        #[test]
        fn test_contains() {
            assert!(contains(&[1, 2, 3], &2));
            assert!(!contains(&[1, 2, 3], &4));
        }
    
        #[test]
        fn test_larger() {
            assert_eq!(larger(10, 20), 20);
            assert_eq!(larger("z", "a"), "z");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_print_item() {
            assert_eq!(print_item(&42), "42");
            assert_eq!(print_item(&"hello"), "hello");
        }
    
        #[test]
        fn test_print_list() {
            assert_eq!(print_list(&[1, 2, 3]), "[1, 2, 3]");
        }
    
        #[test]
        fn test_print_and_clone() {
            let (s, v) = print_and_clone(&42);
            assert_eq!(s, "42");
            assert_eq!(v, 42);
        }
    
        #[test]
        fn test_find_max() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5]), Some(5));
            assert_eq!(find_max::<i32>(&[]), None);
        }
    
        #[test]
        fn test_contains() {
            assert!(contains(&[1, 2, 3], &2));
            assert!(!contains(&[1, 2, 3], &4));
        }
    
        #[test]
        fn test_larger() {
            assert_eq!(larger(10, 20), 20);
            assert_eq!(larger("z", "a"), "z");
        }
    }

    Deep Comparison

    Core Insight

    Rust generics require explicit bounds: <T: Display> means "T must implement Display". OCaml generics are unconstrained — any type works if the operations typecheck structurally.

    OCaml Approach

  • 'a is universally polymorphic — no constraints
  • • Functors for module-level constraints
  • • No trait bounds — structural typing suffices
  • Rust Approach

  • <T: Trait> syntax for inline bounds
  • • Multiple bounds: <T: Display + Clone>
  • • Bounds required to call methods on generic T
  • Comparison Table

    FeatureOCamlRust
    Syntax'a (unconstrained)<T: Bound>
    MultipleN/AT: A + B
    Required?NoYes, to use methods
    CheckedAt use siteAt declaration

    Exercises

  • Median function: Write median<T: PartialOrd + Clone>(mut v: Vec<T>) -> Option<T> that sorts the vector and returns the middle element. What additional bound is needed for sorting?
  • Min and max: Write min_max<T: PartialOrd + Clone>(v: &[T]) -> Option<(T, T)> that returns both the minimum and maximum in a single pass. Use fold with a (T, T) accumulator.
  • Generic statistics: Write Stats<T> that stores count, sum, min, max with bounds T: PartialOrd + Add + Copy + Default. Implement update(&mut self, x: T) that incorporates a new value.
  • Open Source Repos