872-where-clauses — Where Clauses
Tutorial
The Problem
When a generic function involves multiple type parameters, each with several bounds, the inline bound syntax <T: A + B, U: C + D, F: Fn(T) -> U> becomes unwieldy. Rust's where clause separates the type parameter list from the constraints, moving them to a dedicated block after the function signature. This improves readability for complex higher-order functions, especially those accepting multiple function parameters. OCaml achieves the same clarity through structural typing and module signatures, which do not require listing constraints inline.
🎯 Learning Outcomes
where clauseswhere clauses are required (trait bounds on associated types)where clause style for readabilitywhere clauses scale to real-world generic combinatorsCode Example
fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
slice.iter().reduce(|a, b| if a >= b { a } else { b })
}Key Differences
where clauses follow the function signature; OCaml constraints appear in module type signatures used as functor parameters.where is mandatory when constraining associated types (e.g., where T::Item: Display); OCaml handles this via module type refinement.where when there are more than two bounds or multiple type parameters; OCaml has no equivalent guideline.OCaml Approach
OCaml achieves constraint clarity through structural module types. A functor MakeProcessor(M: Mappable) separates the constraint (the module signature) from the implementation. For plain functions, OCaml uses implicit structural polymorphism or explicit function parameters like ~transform ~combine ~init. Since OCaml doesn't have inline bound syntax, all constraints are implicitly structural — there is no equivalent to the where clause syntactic distinction.
Full Source
#![allow(clippy::all)]
// Example 078: Where Clauses
// Complex where clauses vs inline bounds
use std::fmt::{Debug, Display};
use std::ops::{Add, Mul};
// === Approach 1: Inline bounds (simple cases) ===
fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
slice.iter().reduce(|a, b| if a >= b { a } else { b })
}
// === Approach 2: Where clauses (complex constraints) ===
fn transform_and_combine<T, U, A, F, G>(items: &[T], transform: F, combine: G, init: A) -> A
where
F: Fn(&T) -> U,
G: Fn(A, U) -> A,
{
items.iter().fold(init, |acc, x| combine(acc, transform(x)))
}
fn filter_map_fold<T, U, A, P, F, G>(items: &[T], pred: P, transform: F, combine: G, init: A) -> A
where
P: Fn(&T) -> bool,
F: Fn(&T) -> U,
G: Fn(A, U) -> A,
{
items.iter().fold(init, |acc, x| {
if pred(x) {
combine(acc, transform(x))
} else {
acc
}
})
}
// Where clause shines with multiple related bounds
fn sorted_summary<T>(items: &mut [T]) -> String
where
T: Ord + Display,
{
items.sort();
items
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
}
// === Approach 3: Complex multi-type where clauses ===
fn bounded_transform<T, F>(items: &[T], transform: F, lo: T, hi: T) -> Vec<T>
where
T: PartialOrd + Clone,
F: Fn(&T) -> T,
{
items
.iter()
.map(|x| {
let y = transform(x);
if y < lo {
lo.clone()
} else if y > hi {
hi.clone()
} else {
y
}
})
.collect()
}
// Return type bounds in where clause
fn numeric_summary<T>(a: T, b: T) -> String
where
T: Add<Output = T> + Mul<Output = T> + Display + Copy,
{
let sum = a + b;
let product = a * b;
format!("sum={}, product={}", sum, product)
}
// Where clause with lifetime + trait bounds
fn longest_display<'a, T>(a: &'a T, b: &'a T) -> String
where
T: Display + PartialOrd,
{
if a >= b {
format!("{}", a)
} else {
format!("{}", b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transform_and_combine() {
let r = transform_and_combine(&[1, 2, 3, 4], |x| x * x, |a, b| a + b, 0);
assert_eq!(r, 30);
}
#[test]
fn test_filter_map_fold() {
let r = filter_map_fold(
&[1, 2, 3, 4, 5, 6],
|x| x % 2 == 0,
|x| x * x,
|a, b| a + b,
0,
);
assert_eq!(r, 56); // 4 + 16 + 36
}
#[test]
fn test_sorted_summary() {
let mut v = vec![3, 1, 4, 1, 5];
assert_eq!(sorted_summary(&mut v), "1, 1, 3, 4, 5");
}
#[test]
fn test_bounded_transform() {
let r = bounded_transform(&[1, 2, 3, 4, 5], |x| x * 3, 0, 10);
assert_eq!(r, vec![3, 6, 9, 10, 10]);
}
#[test]
fn test_numeric_summary() {
assert_eq!(numeric_summary(3, 4), "sum=7, product=12");
}
#[test]
fn test_longest_display() {
assert_eq!(longest_display(&10, &20), "20");
assert_eq!(longest_display(&"zebra", &"apple"), "zebra");
}
#[test]
fn test_empty_slice() {
let r = transform_and_combine::<i32, i32, i32, _, _>(&[], |x| x * x, |a, b| a + b, 0);
assert_eq!(r, 0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transform_and_combine() {
let r = transform_and_combine(&[1, 2, 3, 4], |x| x * x, |a, b| a + b, 0);
assert_eq!(r, 30);
}
#[test]
fn test_filter_map_fold() {
let r = filter_map_fold(
&[1, 2, 3, 4, 5, 6],
|x| x % 2 == 0,
|x| x * x,
|a, b| a + b,
0,
);
assert_eq!(r, 56); // 4 + 16 + 36
}
#[test]
fn test_sorted_summary() {
let mut v = vec![3, 1, 4, 1, 5];
assert_eq!(sorted_summary(&mut v), "1, 1, 3, 4, 5");
}
#[test]
fn test_bounded_transform() {
let r = bounded_transform(&[1, 2, 3, 4, 5], |x| x * 3, 0, 10);
assert_eq!(r, vec![3, 6, 9, 10, 10]);
}
#[test]
fn test_numeric_summary() {
assert_eq!(numeric_summary(3, 4), "sum=7, product=12");
}
#[test]
fn test_longest_display() {
assert_eq!(longest_display(&10, &20), "20");
assert_eq!(longest_display(&"zebra", &"apple"), "zebra");
}
#[test]
fn test_empty_slice() {
let r = transform_and_combine::<i32, i32, i32, _, _>(&[], |x| x * x, |a, b| a + b, 0);
assert_eq!(r, 0);
}
}
Deep Comparison
Comparison: Where Clauses
Inline vs Where Clause
Rust — Inline bounds (simple):
fn find_max<T: PartialOrd>(slice: &[T]) -> Option<&T> {
slice.iter().reduce(|a, b| if a >= b { a } else { b })
}
Rust — Where clause (complex):
fn transform_and_combine<T, U, A, F, G>(items: &[T], transform: F, combine: G, init: A) -> A
where
F: Fn(&T) -> U,
G: Fn(A, U) -> A,
{
items.iter().fold(init, |acc, x| combine(acc, transform(x)))
}
OCaml Equivalent — No Explicit Constraints
OCaml:
let transform_and_combine ~transform ~combine ~init items =
List.fold_left (fun acc x -> combine acc (transform x)) init items
(* Types are fully inferred, no constraints written *)
Rust:
fn transform_and_combine<T, U, A, F, G>(items: &[T], transform: F, combine: G, init: A) -> A
where F: Fn(&T) -> U, G: Fn(A, U) -> A,
{ /* ... */ }
Multiple Related Bounds
OCaml:
let sorted_summary items to_str =
let sorted = List.sort compare items in
String.concat ", " (List.map to_str sorted)
Rust:
fn sorted_summary<T>(items: &mut [T]) -> String
where
T: Ord + Display,
{
items.sort();
items.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", ")
}
Exercises
merge_maps<K, V, F> function using a where clause where K: Ord + Clone, V: Clone, and F: Fn(V, V) -> V merges duplicate keys.pipeline<T, F1, F2, F3> that chains three transformations, using a where clause to bound each Fi: Fn(T) -> T.transform_and_combine using inline bounds and compare readability — document which style you prefer and why.