18.7 Higher-Ranked Trait Bounds: for<'a>
Alright, let’s talk about Higher-Ranked Trait Bounds, or HRTBs for short. You’ve seen for<'a> syntax and maybe your eyes glazed over. I get it. It looks like arcane incantation, something you’d mumble while sacrificing a goat to the compiler gods. But trust me, it’s not magic. It’s just the way we tell Rust, “Look, I need a function that’s cool with any lifetime you throw at it, not just one specific one.”
Think of it this way: most of the time, when we write a function that takes a reference, the lifetime of that reference is fixed relative to the function’s arguments or the output. It’s a specific relationship. HRTBs are for when you need to be more general than that. You’re not saying “for this specific lifetime 'a,” you’re saying “for all possible lifetimes 'a.” This is crucial when you’re dealing with closures and traits that can be implemented for references of any lifetime.
The Classic Pitfall: The Fn Trait
Here’s where you’ll most commonly trip over this and why you need HRTBs. Let’s say you want to write a function that takes a closure. This closure must be able to take a string slice reference with some lifetime. You might try this:
fn call_with_hello<F>(f: F)
where
F: Fn(&str),
{
f("hello");
}
Seems fine, right? It compiles. But let’s try to use it in a slightly more complex scenario. What if the closure we pass in needs to accept a reference with a lifetime that isn’t just 'static?
fn main() {
let s = String::from("world");
let closure = |arg: &str| {
println!("{}", arg);
// What if we wanted to compare `arg` to `&s` here?
// The compiler would start asking about lifetimes.
};
call_with_hello(closure); // This is okay.
}
The problem isn’t in this simple example; it’s in the type of the closure. When we write F: Fn(&str), what we’re actually saying is F: for<'a> Fn(&'a str). Read that carefully. The for<'a> is implicit here. We are saying “The closure F must implement Fn for a string reference with any lifetime 'a.” This is a higher-ranked requirement!
Our call_with_hello function works because the closure we defined does happen to work with any lifetime. The string literal "hello" has the 'static lifetime, and our closure can handle that.
Now, watch what happens when the closure isn’t so generic. This is the moment the designers’ questionable choice of implicit bounds becomes a bit of a pain.
// This function takes a closure that is only valid for a specific lifetime `'a`
fn bad_function<F>(f: F)
where
// Note: Missing `for<'a>`! This is the wrong way.
F: Fn(&str), // This is actually `for<'a> Fn(&'a str)`!
{
f("hello"); // We try to give it a `&'static str`
}
fn create_closure() -> impl Fn(&str) {
let my_string = String::from("a");
// This closure is now tied to the lifetime of `my_string` inside this function.
// It can only accept references that live at least as long as `my_string`.
// Its type is roughly `for<'a> where 'a: 'b, Fn(&'a str)`, which is NOT
// the same as `for<'a> Fn(&'a str)`.
move |arg: &str| {
println!("Compare them: {} and {}", my_string, arg);
}
}
// `my_string` drops here, so the returned closure is invalid. This won't compile.
The issue is that the returned closure has a hidden lifetime requirement. It can’t accept just any &str; it can only accept ones that live long enough for its internal logic (which, in this broken example, involves a dropped value). Our bad_function requires a closure that is truly generic over all lifetimes, and our create_closure doesn’t provide that.
Enter for<'a> Explicitly
So when do you need to write for<'a> yourself? Primarily when you’re defining traits that have methods which take references, or when you’re taking a function pointer (not a closure) that must be lifetime-agnostic.
The most important real-world use case is with the Fn traits themselves. The standard library defines them using HRTBs. If you ever need to bound a type by a closure that takes a reference with any lifetime, you write it explicitly:
fn work_with_callback<F>(f: F)
where
// This is the explicit, full form of what we implicitly wrote earlier.
F: for<'a> Fn(&'a str) -> &'a str, // "For any lifetime 'a, F must be callable..."
{
let result = f("hello");
println!("{}", result);
}
fn main() {
// A closure that trivially satisfies `for<'a> Fn(&'a str) -> &'a str`
let trimmer = |s: &str| s.trim();
work_with_callback(trimmer);
}
The key insight is that for<'a> Fn(&'a str) is a more general bound than Fn(&'some_specific_str). The first can accept the second, but not the other way around. HRTBs let you describe that necessary generality at the trait bound level, ensuring the functions and closures you accept are truly flexible enough for your code’s requirements. It’s the compiler’s way of making you promise that your function isn’t going to be picky about its arguments’ lifetimes.