Higher-Ranked Trait Bounds (for<'a>)
Tutorial Video
Text description (accessibility)
This video demonstrates the "Higher-Ranked Trait Bounds (for<'a>)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Standard lifetime parameters on functions are monomorphic: `F: Fn(&'a str) -> &'a str` means `F` works for one specific lifetime `'a` chosen by the caller. Key difference from OCaml: 1. **Implicit vs explicit**: In Rust, `Fn(&T)
Tutorial
The Problem
Standard lifetime parameters on functions are monomorphic: F: Fn(&'a str) -> &'a str means F works for one specific lifetime 'a chosen by the caller. But some abstractions need functions that work for any lifetime — a callback that processes strings of any duration, not just one specific scope. Higher-Ranked Trait Bounds (HRTB) with for<'a> express universal quantification over lifetimes: F: for<'a> Fn(&'a str) -> &'a str means F must work for every possible lifetime. This is essential for trait objects, parser combinators, and middleware that receive arbitrarily-scoped references.
🎯 Learning Outcomes
for<'a> means: the bound must hold for all lifetimes simultaneouslyapply_hrtb<F: for<'a> Fn(&'a str) -> &'a str> differs from a fixed-lifetime versionF: Fn(&T) -> &Ttrait Processor)Iterator::for_each, serde deserializersCode Example
// HRTB: for<'a> quantifies over all lifetimes
pub fn apply_hrtb<F>(f: F, s: &str) -> String
where
F: for<'a> Fn(&'a str) -> &'a str,
{
f(s).to_string()
}
// Common use: callbacks that work on any borrow
fn transform<F>(f: F) where F: for<'a> Fn(&'a T) -> &'a TKey Differences
Fn(&T) -> &T implicitly introduces for<'a> — it is commonly written without explicit for<'a>; the syntax only appears when necessary for clarity.for<'a> achieves this for lifetime polymorphism.impl Fn(&str) implicitly means impl for<'a> Fn(&'a str).for<'a> without explaining it well.OCaml Approach
OCaml's HM type system achieves similar genericity through polymorphism. A function that processes strings of any kind is simply:
let apply f s = f s (* works for any 'a -> 'b *)
let transform_all f items = List.map f items
OCaml's rank-2 polymorphism (for functions that must be polymorphic in their arguments) requires explicit type annotations with forall using the module system or record wrapping.
Full Source
#![allow(clippy::all)]
//! Higher-Ranked Trait Bounds (for<'a>)
//!
//! Universal quantification over lifetimes for flexible callbacks.
/// Without HRTB: lifetime fixed at call site.
pub fn apply_fixed<'a, F>(f: F, s: &'a str) -> &'a str
where
F: Fn(&'a str) -> &'a str,
{
f(s)
}
/// With HRTB: F works for ANY lifetime.
pub fn apply_hrtb<F>(f: F, s: &str) -> String
where
F: for<'a> Fn(&'a str) -> &'a str,
{
f(s).to_string()
}
/// HRTB in trait definitions.
pub trait Processor {
fn process<'a>(&self, input: &'a str) -> &'a str;
}
/// Common HRTB pattern: Fn(&T) -> &T.
pub fn transform_all<F>(items: &[String], f: F) -> Vec<String>
where
F: for<'a> Fn(&'a str) -> &'a str,
{
items.iter().map(|s| f(s).to_string()).collect()
}
/// Identity processor (for<'a> Fn(&'a str) -> &'a str).
pub fn identity(s: &str) -> &str {
s
}
/// Trim processor.
pub fn trim(s: &str) -> &str {
s.trim()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_fixed() {
let s = String::from(" hello ");
let result = apply_fixed(|x| x.trim(), &s);
assert_eq!(result, "hello");
}
#[test]
fn test_apply_hrtb() {
let s = " world ";
let result = apply_hrtb(|x| x.trim(), s);
assert_eq!(result, "world");
}
#[test]
fn test_transform_all() {
let items = vec![String::from(" a "), String::from(" b ")];
let result = transform_all(&items, trim);
assert_eq!(result, vec!["a", "b"]);
}
#[test]
fn test_identity() {
let s = "hello";
assert_eq!(identity(s), "hello");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_fixed() {
let s = String::from(" hello ");
let result = apply_fixed(|x| x.trim(), &s);
assert_eq!(result, "hello");
}
#[test]
fn test_apply_hrtb() {
let s = " world ";
let result = apply_hrtb(|x| x.trim(), s);
assert_eq!(result, "world");
}
#[test]
fn test_transform_all() {
let items = vec![String::from(" a "), String::from(" b ")];
let result = transform_all(&items, trim);
assert_eq!(result, vec!["a", "b"]);
}
#[test]
fn test_identity() {
let s = "hello";
assert_eq!(identity(s), "hello");
}
}
Deep Comparison
OCaml vs Rust: Higher-Ranked Types
OCaml
(* Rank-2 polymorphism via records *)
type 'a processor = { process: 'b. 'b -> 'a }
(* Or via first-class modules *)
let apply (type a) (f : a -> a) (x : a) = f x
Rust
// HRTB: for<'a> quantifies over all lifetimes
pub fn apply_hrtb<F>(f: F, s: &str) -> String
where
F: for<'a> Fn(&'a str) -> &'a str,
{
f(s).to_string()
}
// Common use: callbacks that work on any borrow
fn transform<F>(f: F) where F: for<'a> Fn(&'a T) -> &'a T
Key Differences
Exercises
fn apply_twice<F: for<'a> Fn(&'a str) -> &'a str>(f: F, s: &str) -> String that applies f to s twice, returning the result of the second application.Processor trait for a TrimProcessor struct and use it in transform_all — verify it works with strings of any lifetime.Box<dyn for<'a> Fn(&'a str) -> &'a str> in a struct and call it with references of different lifetimes — show the boxed closure satisfies the HRTB.