393: Trait Bounds and Where Clauses
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
T supportsT: A + B) and where clause formwhere clauses improve readability (multiple parameters, long bounds, associated type bounds)T: Debug + Clone, lifetime bounds T: 'a)longest_with_debug-style functionsCode 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
impl the trait; OCaml constraints are structural — any module providing the required functions satisfies the constraint.where clause directly mirrors OCaml's functor parameter style; both achieve separation of concerns between signature and constraints.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(">="));
}
}#[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
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!.struct Cache<K, V> where K: Eq + Hash + Clone and V: Clone. Use where clauses throughout. Add get, insert, and invalidate_all methods.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.