077 — Generic Bounds
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
T: Display+: T: Display + CloneT: std::iter::Sum + CopyCode 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
T: Display finds the implementation automatically. OCaml passes trait dictionaries (module arguments) explicitly in the traditional style.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.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);
}
}#[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 constraintsRust Approach
<T: Trait> syntax for inline bounds<T: Display + Clone>Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Syntax | 'a (unconstrained) | <T: Bound> |
| Multiple | N/A | T: A + B |
| Required? | No | Yes, to use methods |
| Checked | At use site | At declaration |
Exercises
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_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.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.