Rust Higher-Ranked Trait Bounds: for<'a> Syntax
Rust's lifetime system usually handles borrowing elegantly, but there's a class of problems where standard lifetime bounds fall short. Consider writing a function that accepts a closure operating on...
Key Insights
- Higher-ranked trait bounds (HRTBs) let you express “for all lifetimes” constraints, essential when closures or traits must work with borrowed data of any lifetime rather than one specific lifetime
- The
for<'a>syntax enables universal quantification over lifetimes—think of it as a lifetime parameter that the caller chooses, not the implementer - Most “implementation is not general enough” errors stem from missing HRTBs where the compiler needs proof your code works for every possible lifetime, not just one
Introduction to the Problem
Rust’s lifetime system usually handles borrowing elegantly, but there’s a class of problems where standard lifetime bounds fall short. Consider writing a function that accepts a closure operating on string slices:
fn process_strings<F>(data: Vec<String>, predicate: F) -> Vec<String>
where
F: Fn(&str) -> bool,
{
data.into_iter()
.filter(|s| predicate(s.as_str()))
.collect()
}
This compiles fine. But what if you need to store that closure in a struct, or work with more complex lifetime scenarios?
struct StringProcessor<F> {
predicate: F,
}
impl<F> StringProcessor<F>
where
F: Fn(&str) -> bool, // This won't work for all cases!
{
fn process(&self, input: &str) -> bool {
(self.predicate)(input)
}
}
The issue here is subtle. When you write Fn(&str) -> bool, the compiler infers a hidden lifetime parameter. But that lifetime gets fixed at the point where the trait bound is declared, not where it’s used. If you need the closure to work with string slices of different lifetimes across multiple calls, you need higher-ranked trait bounds.
Understanding Lifetime Bounds vs. Higher-Ranked Bounds
The distinction between T: Trait<'a> and T: for<'a> Trait<'a> is fundamental:
// Concrete lifetime: the lifetime 'a is chosen by the function signature
fn concrete_bound<'a, F>(f: F, data: &'a str)
where
F: Fn(&'a str) -> bool,
{
f(data);
}
// Higher-ranked bound: F must work for ANY lifetime the caller provides
fn higher_ranked_bound<F>(f: F)
where
F: for<'a> Fn(&'a str) -> bool,
{
let local = String::from("temporary");
f(local.as_str()); // Works with local lifetime
let static_str = "static string";
f(static_str); // Also works with 'static
}
With concrete_bound, the lifetime 'a is determined when you call the function. The closure must accept that specific lifetime. With higher_ranked_bound, the closure must accept any lifetime—the bound is universally quantified.
Here’s where it matters in practice:
// Without HRTB - this compiles
fn without_hrtb<'a>(closure: impl Fn(&'a str) -> usize, input: &'a str) -> usize {
closure(input)
}
// With HRTB - more flexible
fn with_hrtb(closure: impl for<'a> Fn(&'a str) -> usize) -> (usize, usize) {
let s1 = String::from("first");
let s2 = String::from("second");
(closure(&s1), closure(&s2)) // Two different lifetimes!
}
The second version is more powerful because the closure can be called with arguments of different lifetimes. This flexibility is crucial for generic abstractions.
The for<'a> Syntax Explained
The for<'a> syntax introduces a universally quantified lifetime. Read it as “for all lifetimes ‘a”. It’s similar to generic type parameters but operates at the lifetime level:
// Explicit HRTB on a trait bound
fn apply_predicate<F>(f: F, strings: Vec<&str>) -> Vec<bool>
where
F: for<'a> Fn(&'a str) -> bool,
{
strings.into_iter().map(|s| f(s)).collect()
}
// HRTB on a trait implementation
trait Validator {
fn validate<'a>(&self, input: &'a str) -> bool;
}
fn use_validator<V>(validator: V)
where
V: for<'a> Validator, // Rarely needed; usually implied
{
// validator can validate strings of any lifetime
}
The for<'a> syntax can appear in trait bounds, type aliases, and trait object definitions:
// Type alias with HRTB
type StringPredicate = Box<dyn for<'a> Fn(&'a str) -> bool>;
// Function returning a trait object with HRTB
fn make_length_checker() -> StringPredicate {
Box::new(|s: &str| s.len() > 5)
}
Common Use Cases
HRTBs shine in higher-order functions and generic abstractions. Here’s a practical example—a function that applies a transformation to multiple data sources:
fn apply_to_strings<F>(sources: &[String], mut operation: F)
where
F: for<'a> FnMut(&'a str),
{
for source in sources {
operation(source.as_str());
}
// Can also work with string literals
operation("additional string");
}
// Usage
fn main() {
let data = vec![
String::from("hello"),
String::from("world"),
];
apply_to_strings(&data, |s| {
println!("Processing: {}", s);
});
}
Another common pattern is with iterator adaptors:
trait StringIteratorExt: Iterator<Item = String> {
fn filter_by<F>(self, predicate: F) -> impl Iterator<Item = String>
where
Self: Sized,
F: for<'a> Fn(&'a str) -> bool,
{
self.filter(move |s| predicate(s.as_str()))
}
}
impl<I: Iterator<Item = String>> StringIteratorExt for I {}
// Usage
fn main() {
let strings = vec![String::from("short"), String::from("longer string")];
let filtered: Vec<_> = strings
.into_iter()
.filter_by(|s| s.len() > 5)
.collect();
}
HRTBs with Multiple Lifetimes
When dealing with multiple borrowed parameters, you need multiple lifetime parameters in your HRTB:
fn compare_pairs<F>(pairs: &[(String, String)], comparator: F) -> Vec<bool>
where
F: for<'a, 'b> Fn(&'a str, &'b str) -> bool,
{
pairs
.iter()
.map(|(a, b)| comparator(a.as_str(), b.as_str()))
.collect()
}
// Usage
fn main() {
let pairs = vec![
(String::from("apple"), String::from("banana")),
(String::from("cat"), String::from("dog")),
];
let results = compare_pairs(&pairs, |a, b| a.len() < b.len());
println!("{:?}", results); // [true, false]
}
The for<'a, 'b> syntax means the closure must work for any combination of lifetimes—the two string slices can have completely independent lifetimes.
Common Pitfalls and Error Messages
The most frequent HRTB error is “implementation of [trait] is not general enough”:
// Broken code
fn broken_example<F>(f: F)
where
F: Fn(&str) -> usize, // Missing HRTB
{
let s1 = String::from("first");
let s2 = String::from("second");
let _ = f(&s1);
let _ = f(&s2); // May fail if lifetimes differ
}
Error message:
error: implementation of `FnOnce` is not general enough
--> src/main.rs:XX:YY
|
| let _ = f(&s2);
| ^^^^^^ implementation of `FnOnce` is not general enough
|
= note: closure with signature `fn(&'a str) -> usize` must implement `FnOnce<(&str,)>`, for any lifetime `'a`...
The fix:
// Corrected version
fn fixed_example<F>(f: F)
where
F: for<'a> Fn(&'a str) -> usize, // Added HRTB
{
let s1 = String::from("first");
let s2 = String::from("second");
let _ = f(&s1);
let _ = f(&s2); // Now works!
}
Another pitfall is overusing HRTBs when storing closures:
// This won't compile - can't store HRTBs directly in most cases
struct Container<F> {
operation: F,
}
impl<F> Container<F>
where
F: for<'a> Fn(&'a str),
{
fn new(operation: F) -> Self {
Container { operation }
}
fn execute(&self, input: &str) {
(self.operation)(input);
}
}
This actually works in many cases, but when it doesn’t, you might need trait objects or different design patterns.
When NOT to Use HRTBs
HRTBs add complexity. Don’t use them when simpler lifetime bounds suffice:
// Over-engineered with HRTB
fn over_engineered<F>(data: &str, f: F)
where
F: for<'a> Fn(&'a str) -> bool,
{
f(data);
}
// Simpler and clearer
fn simple<'a, F>(data: &'a str, f: F)
where
F: Fn(&'a str) -> bool,
{
f(data);
}
If your function only calls the closure with one lifetime, use the simpler form. HRTBs are for when you need to call the closure with multiple different lifetimes or when you’re building generic abstractions that others will use.
Similarly, when the lifetime relationship is clear and fixed, explicit parameters are more readable:
// Clear lifetime relationship
fn process_related<'a>(input: &'a str, transformer: impl Fn(&'a str) -> String) -> String {
transformer(input)
}
Use HRTBs when you’re building libraries, working with complex generic code, or when the compiler explicitly tells you “implementation is not general enough.” For application code with straightforward lifetime relationships, stick to explicit lifetime parameters—they’re easier to understand and maintain.
The for<'a> syntax is a powerful tool for expressing universal lifetime constraints, but like all powerful tools, it should be used judiciously. When you need it, it’s indispensable. When you don’t, it’s just noise.