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

    //! 077: Generic Bounds — constraining generic type parameters with traits.
    //!
    //! OCaml's `'a` is universally polymorphic: any type goes as long as the body
    //! typechecks. Rust requires the caller's promises to be spelled out — a
    //! generic `T` can only invoke the methods granted by its trait bounds.
    //!
    //! Three slices of the idea:
    //! 1. No bounds at all: `identity`, `make_pair`, `swap` work for any `T`.
    //! 2. Single bound: `print_item<T: Display>` needs formatting.
    //! 3. Combined bounds: `find_max<T: PartialOrd + Clone>`, `sum_items<T: Sum + Copy>`.
    
    use std::fmt::Display;
    use std::iter::Sum;
    
    /// Identity — no bound, nothing to require.
    pub fn identity<T>(x: T) -> T {
        x
    }
    
    /// Pair two values of possibly different types.
    pub fn make_pair<A, B>(a: A, b: B) -> (A, B) {
        (a, b)
    }
    
    /// Swap the components of a pair.
    pub fn swap<A, B>(pair: (A, B)) -> (B, A) {
        (pair.1, pair.0)
    }
    
    /// Format a single item — `Display` is what unlocks `{}`.
    pub fn print_item<T: Display>(item: &T) -> String {
        format!("{}", item)
    }
    
    /// Format a slice as `[a; b; c]` — matches the OCaml functor's output.
    pub fn print_list<T: Display>(items: &[T]) -> String {
        let parts: Vec<String> = items.iter().map(|x| format!("{}", x)).collect();
        format!("[{}]", parts.join("; "))
    }
    
    /// Two bounds combined: format the value and hand back a clone.
    pub fn print_and_clone<T: Display + Clone>(item: &T) -> (String, T) {
        (format!("{}", item), item.clone())
    }
    
    /// Largest element, `None` on an empty slice. `PartialOrd` for `>=`,
    /// `Clone` because we return an owned `T`.
    pub fn find_max<T: PartialOrd + Clone>(items: &[T]) -> Option<T> {
        items
            .iter()
            .cloned()
            .reduce(|a, b| if a >= b { a } else { b })
    }
    
    /// Sum of a slice. `Sum` is what `.sum()` demands; `Copy` lets us iterate by value.
    pub fn sum_items<T: Sum + Copy>(items: &[T]) -> T {
        items.iter().copied().sum()
    }
    
    /// Membership test — `PartialEq` is the bound that unlocks `==`.
    pub fn contains<T: PartialEq>(items: &[T], target: &T) -> bool {
        items.iter().any(|x| x == target)
    }
    
    /// The larger of two values. Using a `where` clause for variety — equivalent
    /// to `fn larger<T: PartialOrd>(...)`.
    pub fn larger<T>(a: T, b: T) -> T
    where
        T: PartialOrd,
    {
        if a >= b {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn identity_preserves_value() {
            assert_eq!(identity(42), 42);
            assert_eq!(identity("hello"), "hello");
        }
    
        #[test]
        fn make_pair_accepts_heterogeneous_types() {
            assert_eq!(make_pair(1, "a"), (1, "a"));
        }
    
        #[test]
        fn swap_flips_components() {
            assert_eq!(swap((1, 2)), (2, 1));
            assert_eq!(swap(("x", true)), (true, "x"));
        }
    
        #[test]
        fn print_item_uses_display() {
            assert_eq!(print_item(&42), "42");
            assert_eq!(print_item(&"hello"), "hello");
        }
    
        #[test]
        fn print_list_matches_ocaml_functor_output() {
            assert_eq!(print_list(&[1, 2, 3]), "[1; 2; 3]");
            assert_eq!(print_list::<i32>(&[]), "[]");
        }
    
        #[test]
        fn print_and_clone_returns_both() {
            let (s, v) = print_and_clone(&42);
            assert_eq!(s, "42");
            assert_eq!(v, 42);
        }
    
        #[test]
        fn find_max_on_non_empty_slice() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5, 9, 2, 6]), Some(9));
            assert_eq!(find_max(&["a", "c", "b"]), Some("c"));
        }
    
        #[test]
        fn find_max_on_empty_slice_is_none() {
            assert_eq!(find_max::<i32>(&[]), None);
        }
    
        #[test]
        fn sum_items_uses_sum_trait() {
            assert_eq!(sum_items(&[1, 2, 3, 4]), 10);
            assert_eq!(sum_items::<f64>(&[1.5, 2.5]), 4.0);
        }
    
        #[test]
        fn contains_uses_partial_eq() {
            assert!(contains(&[1, 2, 3], &2));
            assert!(!contains(&[1, 2, 3], &4));
            assert!(contains(&["a", "b", "c"], &"b"));
        }
    
        #[test]
        fn larger_picks_the_bigger_value() {
            assert_eq!(larger(10, 20), 20);
            assert_eq!(larger("z", "a"), "z");
            assert_eq!(larger(1.5, 0.5), 1.5);
        }
    }

    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

    //! 077: Generic Bounds — constraining generic type parameters with traits.
    //!
    //! OCaml's `'a` is universally polymorphic: any type goes as long as the body
    //! typechecks. Rust requires the caller's promises to be spelled out — a
    //! generic `T` can only invoke the methods granted by its trait bounds.
    //!
    //! Three slices of the idea:
    //! 1. No bounds at all: `identity`, `make_pair`, `swap` work for any `T`.
    //! 2. Single bound: `print_item<T: Display>` needs formatting.
    //! 3. Combined bounds: `find_max<T: PartialOrd + Clone>`, `sum_items<T: Sum + Copy>`.
    
    use std::fmt::Display;
    use std::iter::Sum;
    
    /// Identity — no bound, nothing to require.
    pub fn identity<T>(x: T) -> T {
        x
    }
    
    /// Pair two values of possibly different types.
    pub fn make_pair<A, B>(a: A, b: B) -> (A, B) {
        (a, b)
    }
    
    /// Swap the components of a pair.
    pub fn swap<A, B>(pair: (A, B)) -> (B, A) {
        (pair.1, pair.0)
    }
    
    /// Format a single item — `Display` is what unlocks `{}`.
    pub fn print_item<T: Display>(item: &T) -> String {
        format!("{}", item)
    }
    
    /// Format a slice as `[a; b; c]` — matches the OCaml functor's output.
    pub fn print_list<T: Display>(items: &[T]) -> String {
        let parts: Vec<String> = items.iter().map(|x| format!("{}", x)).collect();
        format!("[{}]", parts.join("; "))
    }
    
    /// Two bounds combined: format the value and hand back a clone.
    pub fn print_and_clone<T: Display + Clone>(item: &T) -> (String, T) {
        (format!("{}", item), item.clone())
    }
    
    /// Largest element, `None` on an empty slice. `PartialOrd` for `>=`,
    /// `Clone` because we return an owned `T`.
    pub fn find_max<T: PartialOrd + Clone>(items: &[T]) -> Option<T> {
        items
            .iter()
            .cloned()
            .reduce(|a, b| if a >= b { a } else { b })
    }
    
    /// Sum of a slice. `Sum` is what `.sum()` demands; `Copy` lets us iterate by value.
    pub fn sum_items<T: Sum + Copy>(items: &[T]) -> T {
        items.iter().copied().sum()
    }
    
    /// Membership test — `PartialEq` is the bound that unlocks `==`.
    pub fn contains<T: PartialEq>(items: &[T], target: &T) -> bool {
        items.iter().any(|x| x == target)
    }
    
    /// The larger of two values. Using a `where` clause for variety — equivalent
    /// to `fn larger<T: PartialOrd>(...)`.
    pub fn larger<T>(a: T, b: T) -> T
    where
        T: PartialOrd,
    {
        if a >= b {
            a
        } else {
            b
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn identity_preserves_value() {
            assert_eq!(identity(42), 42);
            assert_eq!(identity("hello"), "hello");
        }
    
        #[test]
        fn make_pair_accepts_heterogeneous_types() {
            assert_eq!(make_pair(1, "a"), (1, "a"));
        }
    
        #[test]
        fn swap_flips_components() {
            assert_eq!(swap((1, 2)), (2, 1));
            assert_eq!(swap(("x", true)), (true, "x"));
        }
    
        #[test]
        fn print_item_uses_display() {
            assert_eq!(print_item(&42), "42");
            assert_eq!(print_item(&"hello"), "hello");
        }
    
        #[test]
        fn print_list_matches_ocaml_functor_output() {
            assert_eq!(print_list(&[1, 2, 3]), "[1; 2; 3]");
            assert_eq!(print_list::<i32>(&[]), "[]");
        }
    
        #[test]
        fn print_and_clone_returns_both() {
            let (s, v) = print_and_clone(&42);
            assert_eq!(s, "42");
            assert_eq!(v, 42);
        }
    
        #[test]
        fn find_max_on_non_empty_slice() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5, 9, 2, 6]), Some(9));
            assert_eq!(find_max(&["a", "c", "b"]), Some("c"));
        }
    
        #[test]
        fn find_max_on_empty_slice_is_none() {
            assert_eq!(find_max::<i32>(&[]), None);
        }
    
        #[test]
        fn sum_items_uses_sum_trait() {
            assert_eq!(sum_items(&[1, 2, 3, 4]), 10);
            assert_eq!(sum_items::<f64>(&[1.5, 2.5]), 4.0);
        }
    
        #[test]
        fn contains_uses_partial_eq() {
            assert!(contains(&[1, 2, 3], &2));
            assert!(!contains(&[1, 2, 3], &4));
            assert!(contains(&["a", "b", "c"], &"b"));
        }
    
        #[test]
        fn larger_picks_the_bigger_value() {
            assert_eq!(larger(10, 20), 20);
            assert_eq!(larger("z", "a"), "z");
            assert_eq!(larger(1.5, 0.5), 1.5);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn identity_preserves_value() {
            assert_eq!(identity(42), 42);
            assert_eq!(identity("hello"), "hello");
        }
    
        #[test]
        fn make_pair_accepts_heterogeneous_types() {
            assert_eq!(make_pair(1, "a"), (1, "a"));
        }
    
        #[test]
        fn swap_flips_components() {
            assert_eq!(swap((1, 2)), (2, 1));
            assert_eq!(swap(("x", true)), (true, "x"));
        }
    
        #[test]
        fn print_item_uses_display() {
            assert_eq!(print_item(&42), "42");
            assert_eq!(print_item(&"hello"), "hello");
        }
    
        #[test]
        fn print_list_matches_ocaml_functor_output() {
            assert_eq!(print_list(&[1, 2, 3]), "[1; 2; 3]");
            assert_eq!(print_list::<i32>(&[]), "[]");
        }
    
        #[test]
        fn print_and_clone_returns_both() {
            let (s, v) = print_and_clone(&42);
            assert_eq!(s, "42");
            assert_eq!(v, 42);
        }
    
        #[test]
        fn find_max_on_non_empty_slice() {
            assert_eq!(find_max(&[3, 1, 4, 1, 5, 9, 2, 6]), Some(9));
            assert_eq!(find_max(&["a", "c", "b"]), Some("c"));
        }
    
        #[test]
        fn find_max_on_empty_slice_is_none() {
            assert_eq!(find_max::<i32>(&[]), None);
        }
    
        #[test]
        fn sum_items_uses_sum_trait() {
            assert_eq!(sum_items(&[1, 2, 3, 4]), 10);
            assert_eq!(sum_items::<f64>(&[1.5, 2.5]), 4.0);
        }
    
        #[test]
        fn contains_uses_partial_eq() {
            assert!(contains(&[1, 2, 3], &2));
            assert!(!contains(&[1, 2, 3], &4));
            assert!(contains(&["a", "b", "c"], &"b"));
        }
    
        #[test]
        fn larger_picks_the_bigger_value() {
            assert_eq!(larger(10, 20), 20);
            assert_eq!(larger("z", "a"), "z");
            assert_eq!(larger(1.5, 0.5), 1.5);
        }
    }

    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