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
#![allow(clippy::all)]
// 078: Where Clauses
// Complex trait bounds using where syntax
use std::fmt::Display;
use std::ops::{Add, Mul};
// Approach 1: Where clause for readability
fn print_if_equal<T>(a: &T, b: &T) -> String
where
T: Display + PartialEq,
{
if a == b {
format!("{} == {}", a, b)
} else {
format!("{} != {}", a, b)
}
}
// Approach 2: Multiple type params with where
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()
}
// Approach 3: Associated type bounds
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)
}
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)
}
// Complex: display collection of displayable items
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 test_print_if_equal() {
assert_eq!(print_if_equal(&5, &5), "5 == 5");
assert_eq!(print_if_equal(&3, &4), "3 != 4");
}
#[test]
fn test_zip_with() {
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 test_sum_items() {
assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
}
#[test]
fn test_dot_product() {
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
}
#[test]
fn test_display_collection() {
assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
}
}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
#![allow(clippy::all)]
// 078: Where Clauses
// Complex trait bounds using where syntax
use std::fmt::Display;
use std::ops::{Add, Mul};
// Approach 1: Where clause for readability
fn print_if_equal<T>(a: &T, b: &T) -> String
where
T: Display + PartialEq,
{
if a == b {
format!("{} == {}", a, b)
} else {
format!("{} != {}", a, b)
}
}
// Approach 2: Multiple type params with where
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()
}
// Approach 3: Associated type bounds
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)
}
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)
}
// Complex: display collection of displayable items
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 test_print_if_equal() {
assert_eq!(print_if_equal(&5, &5), "5 == 5");
assert_eq!(print_if_equal(&3, &4), "3 != 4");
}
#[test]
fn test_zip_with() {
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 test_sum_items() {
assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
}
#[test]
fn test_dot_product() {
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
}
#[test]
fn test_display_collection() {
assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_print_if_equal() {
assert_eq!(print_if_equal(&5, &5), "5 == 5");
assert_eq!(print_if_equal(&3, &4), "3 != 4");
}
#[test]
fn test_zip_with() {
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 test_sum_items() {
assert_eq!(sum_items(vec![1, 2, 3, 4, 5].into_iter()), 15);
}
#[test]
fn test_dot_product() {
assert_eq!(dot_product(&[1, 2, 3], &[4, 5, 6]), 32);
}
#[test]
fn test_display_collection() {
assert_eq!(display_collection(vec![1, 2, 3]), "[1, 2, 3]");
}
}
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.