078 — Where Clauses
Tutorial
The Problem
Express complex trait bounds on generic functions and types using Rust's where clause syntax. Implement print_if_equal, zip_with, sum_items, dot_product, and display_collection — each requiring multiple or compound constraints — and compare with OCaml's module functor approach to constraining polymorphic code.
🎯 Learning Outcomes
where clauses to separate complex bounds from the function signatureT: Display + PartialEq)I::Item: Add + Default)IntoIterator with I::Item: Display for generic collection printingwhere improves readability over inline bound syntaxwhere constraintsCode Example
//! 078: Where Clauses — complex trait bounds in a readable form.
//!
//! Inline bounds (`fn f<T: A + B>()`) work for simple cases. `where`
//! clauses move bounds after the signature, which:
//! - keeps long bound lists from crowding the return type,
//! - spreads constraints across several type parameters cleanly,
//! - is the *only* place associated-type bounds like `I::Item: Display`
//! can be written.
//!
//! OCaml expresses the same idea with module functors and signatures
//! (`module type SUMMABLE = sig … end`); Rust expresses it by constraining
//! generic parameters directly. The constraint surface is equivalent,
//! but Rust's monomorphization keeps it zero-cost at runtime.
use std::fmt::Display;
use std::ops::{Add, Mul};
/// Two bounds on a single parameter — readable either way, but `where`
/// scales when more show up.
pub fn print_if_equal<T>(a: &T, b: &T) -> String
where
T: Display + PartialEq,
{
if a == b {
format!("{} == {}", a, b)
} else {
format!("{} != {}", a, b)
}
}
/// Three unrelated type parameters plus a closure — the canonical case
/// where inline bounds turn illegible.
pub fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
where
A: Clone,
B: Clone,
F: Fn(A, B) -> C,
{
a.iter()
.cloned()
.zip(b.iter().cloned())
.map(|(x, y)| f(x, y))
.collect()
}
/// Associated-type bound — `I::Item: Add + Default` is only spellable in
/// a `where` clause. Mirrors OCaml's `MathOps(S : SUMMABLE).sum`.
pub fn sum_items<I>(iter: I) -> I::Item
where
I: Iterator,
I::Item: Add<Output = I::Item> + Default,
{
iter.fold(I::Item::default(), |acc, x| acc + x)
}
/// Four bounds stacked — the `RING` signature from the OCaml example,
/// expressed as arithmetic + memory bounds on a single `T`.
pub fn dot_product<T>(a: &[T], b: &[T]) -> T
where
T: Add<Output = T> + Mul<Output = T> + Default + Copy,
{
a.iter()
.zip(b.iter())
.fold(T::default(), |acc, (&x, &y)| acc + x * y)
}
/// Generic pretty-printer: accepts any iterable whose items are `Display`.
pub fn display_collection<I>(iter: I) -> String
where
I: IntoIterator,
I::Item: Display,
{
let items: Vec<String> = iter.into_iter().map(|x| format!("{}", x)).collect();
format!("[{}]", items.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_if_equal_formats_both_outcomes() {
assert_eq!(print_if_equal(&5, &5), "5 == 5");
assert_eq!(print_if_equal(&3, &4), "3 != 4");
assert_eq!(print_if_equal(&"a", &"a"), "a == a");
}
#[test]
fn zip_with_combines_two_slices() {
assert_eq!(
zip_with(&[1, 2, 3], &[4, 5, 6], |a, b| a + b),
vec![5, 7, 9]
);
assert_eq!(zip_with(&[1, 2], &[3, 4], |a, b| a * b), vec![3, 8]);
}
#[test]
fn sum_items_uses_associated_type_bound() {
assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
assert_eq!(sum_items::<std::vec::IntoIter<i32>>(vec![].into_iter()), 0);
}
#[test]
fn dot_product_matches_ocaml_int_ring() {
// OCaml: IntRing.dot_product [1; 2; 3] [4; 5; 6] = 32
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
assert_eq!(dot_product::<f64>(&[1.0, 2.0], &[3.0, 4.0]), 11.0);
}
#[test]
fn display_collection_works_on_any_iterable() {
assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
assert_eq!(display_collection(["a", "b", "c"]), "[a, b, c]");
assert_eq!(display_collection(Vec::<i32>::new()), "[]");
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Syntax | where T: Trait1 + Trait2 | module type SIG = sig … end |
| Scope | Per function/impl block | Module-level functor |
| Associated type bounds | I::Item: Trait in where | with type item = t refinement |
| Constraint composition | + on trait bounds | include in module types |
| Monomorphization | Yes, at compile time | Functors instantiated at application |
| Runtime cost | None | None |
where syntax is purely cosmetic in most cases — it does not change semantics versus inline bounds. The exception is associated type constraints, which require where. OCaml functors produce named modules, making them first-class values; Rust generic functions are erased into monomorphized copies.
OCaml Approach
OCaml expresses constraints via module signatures. A functor MathOps(S : SUMMABLE) requires the input module to provide zero, add, and to_string. More complex constraints combine signatures: module type RING = sig include SUMMABLE include MULTIPLIABLE end. Concrete modules like IntSum and FloatSum are produced by applying the functor to struct-style anonymous modules. The type system ensures constraints are satisfied at functor application, not at call sites — equivalent to Rust's monomorphization.
Full Source
//! 078: Where Clauses — complex trait bounds in a readable form.
//!
//! Inline bounds (`fn f<T: A + B>()`) work for simple cases. `where`
//! clauses move bounds after the signature, which:
//! - keeps long bound lists from crowding the return type,
//! - spreads constraints across several type parameters cleanly,
//! - is the *only* place associated-type bounds like `I::Item: Display`
//! can be written.
//!
//! OCaml expresses the same idea with module functors and signatures
//! (`module type SUMMABLE = sig … end`); Rust expresses it by constraining
//! generic parameters directly. The constraint surface is equivalent,
//! but Rust's monomorphization keeps it zero-cost at runtime.
use std::fmt::Display;
use std::ops::{Add, Mul};
/// Two bounds on a single parameter — readable either way, but `where`
/// scales when more show up.
pub fn print_if_equal<T>(a: &T, b: &T) -> String
where
T: Display + PartialEq,
{
if a == b {
format!("{} == {}", a, b)
} else {
format!("{} != {}", a, b)
}
}
/// Three unrelated type parameters plus a closure — the canonical case
/// where inline bounds turn illegible.
pub fn zip_with<A, B, C, F>(a: &[A], b: &[B], f: F) -> Vec<C>
where
A: Clone,
B: Clone,
F: Fn(A, B) -> C,
{
a.iter()
.cloned()
.zip(b.iter().cloned())
.map(|(x, y)| f(x, y))
.collect()
}
/// Associated-type bound — `I::Item: Add + Default` is only spellable in
/// a `where` clause. Mirrors OCaml's `MathOps(S : SUMMABLE).sum`.
pub fn sum_items<I>(iter: I) -> I::Item
where
I: Iterator,
I::Item: Add<Output = I::Item> + Default,
{
iter.fold(I::Item::default(), |acc, x| acc + x)
}
/// Four bounds stacked — the `RING` signature from the OCaml example,
/// expressed as arithmetic + memory bounds on a single `T`.
pub fn dot_product<T>(a: &[T], b: &[T]) -> T
where
T: Add<Output = T> + Mul<Output = T> + Default + Copy,
{
a.iter()
.zip(b.iter())
.fold(T::default(), |acc, (&x, &y)| acc + x * y)
}
/// Generic pretty-printer: accepts any iterable whose items are `Display`.
pub fn display_collection<I>(iter: I) -> String
where
I: IntoIterator,
I::Item: Display,
{
let items: Vec<String> = iter.into_iter().map(|x| format!("{}", x)).collect();
format!("[{}]", items.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_if_equal_formats_both_outcomes() {
assert_eq!(print_if_equal(&5, &5), "5 == 5");
assert_eq!(print_if_equal(&3, &4), "3 != 4");
assert_eq!(print_if_equal(&"a", &"a"), "a == a");
}
#[test]
fn zip_with_combines_two_slices() {
assert_eq!(
zip_with(&[1, 2, 3], &[4, 5, 6], |a, b| a + b),
vec![5, 7, 9]
);
assert_eq!(zip_with(&[1, 2], &[3, 4], |a, b| a * b), vec![3, 8]);
}
#[test]
fn sum_items_uses_associated_type_bound() {
assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
assert_eq!(sum_items::<std::vec::IntoIter<i32>>(vec![].into_iter()), 0);
}
#[test]
fn dot_product_matches_ocaml_int_ring() {
// OCaml: IntRing.dot_product [1; 2; 3] [4; 5; 6] = 32
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
assert_eq!(dot_product::<f64>(&[1.0, 2.0], &[3.0, 4.0]), 11.0);
}
#[test]
fn display_collection_works_on_any_iterable() {
assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
assert_eq!(display_collection(["a", "b", "c"]), "[a, b, c]");
assert_eq!(display_collection(Vec::<i32>::new()), "[]");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_if_equal_formats_both_outcomes() {
assert_eq!(print_if_equal(&5, &5), "5 == 5");
assert_eq!(print_if_equal(&3, &4), "3 != 4");
assert_eq!(print_if_equal(&"a", &"a"), "a == a");
}
#[test]
fn zip_with_combines_two_slices() {
assert_eq!(
zip_with(&[1, 2, 3], &[4, 5, 6], |a, b| a + b),
vec![5, 7, 9]
);
assert_eq!(zip_with(&[1, 2], &[3, 4], |a, b| a * b), vec![3, 8]);
}
#[test]
fn sum_items_uses_associated_type_bound() {
assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
assert_eq!(sum_items::<std::vec::IntoIter<i32>>(vec![].into_iter()), 0);
}
#[test]
fn dot_product_matches_ocaml_int_ring() {
// OCaml: IntRing.dot_product [1; 2; 3] [4; 5; 6] = 32
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
assert_eq!(dot_product::<f64>(&[1.0, 2.0], &[3.0, 4.0]), 11.0);
}
#[test]
fn display_collection_works_on_any_iterable() {
assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
assert_eq!(display_collection(["a", "b", "c"]), "[a, b, c]");
assert_eq!(display_collection(Vec::<i32>::new()), "[]");
}
}
Deep Comparison
Core Insight
Where clauses move trait bounds after the function signature for clarity. Essential when bounds involve associated types, multiple type parameters, or complex relationships.
OCaml Approach
Rust Approach
where T: Trait, U: Trait after signaturewhere I::Item: DisplayComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Simple bound | N/A | <T: Display> |
| Complex bound | Functor signature | where T: A + B, U: C |
| Associated type | Module type | where I::Item: Display |
Exercises
min_max function with signature fn min_max<T>(slice: &[T]) -> Option<(&T, &T)> using a where clause requiring T: PartialOrd.map_collect<I, F, B>(iter: I, f: F) -> Vec<B> using where to express the iterator and closure bounds separately.Printable trait with a print method, then use where T: Printable + Clone in a function that clones and prints each element of a slice.Sorted(C : COMPARABLE) and implement a sorted insertion function. Compare the constraint surface area with the Rust where equivalent.fn f<T: A + B>() and fn f<T>() where T: A + B. Verify both compile identically with cargo expand or by checking the generated MIR.