Higher-ranked types
Another feature of trait objects is that they can be higher-ranked over lifetime parameters of the trait:
// A trait with a lifetime parameter trait Look<'s> { fn method(&self, s: &'s str); } // An implementation that works for any lifetime impl<'s> Look<'s> for () { fn method(&self, s: &'s str) { println!("Hi there, {s}!"); } } fn main() { // A higher-ranked trait object // vvvvvvvvvvvvvvvvvvvvvvvv let _bx: Box<dyn for<'any> Look<'any>> = Box::new(()); }
The for<'x>
part is a lifetime binder that introduces higher-ranked
lifetimes. There can be more than one lifetime, and you can give them
arbitrary names just like lifetime parameters on functions, structs,
and so on.
You can only coerce to a higher-ranked trait object if you implement the trait in question for all lifetimes. For example, this doesn't work:
trait Look<'s> { fn method(&self, s: &'s str); } impl<'s> Look<'s> for &'s i32 { fn method(&self, s: &'s str) { println!("Hi there, {s}!"); } } fn main() { let _bx: Box<dyn for<'any> Look<'any>> = Box::new(&0); }
&'s i32
only implements Look<'s>
, not Look<'a>
for all lifetimes 'a
.
Similarly, this won't work either:
trait Look<'s> { fn method(&self, s: &'s str); } impl Look<'static> for i32 { fn method(&self, s: &'static str) { println!("Hi there, {s}!"); } } fn main() { let _bx: Box<dyn for<'any> Look<'any>> = Box::new(0); }
Implementing the trait with 'static
as the lifetime parameter is not the
same thing as implementing the trait for any lifetime as the parameter.
Traits and trait implementations don't have something like variance; the
parameters of traits are always invariant and thus implementations are
always for the explicit lifetime(s) only.
Subtyping
There's a relationship between higher-ranked types like dyn for<'any> Look<'any>
and non-higher-ranked types like dyn Look<'x>
(for a single lifetime 'x
): the
higher-ranked type is a subtype of the non-higher-ranked types. Thus you can
coerce a higher-ranked type to a non-higher-ranked type with any concrete lifetime:
#![allow(unused)] fn main() { trait Look<'s> { fn method(&self, s: &'s str); } fn as_static(bx: Box<dyn for<'any> Look<'any>>) -> Box<dyn Look<'static>> { bx } fn as_whatever<'w>(bx: Box<dyn for<'any> Look<'any>>) -> Box<dyn Look<'w>> { bx } }
Note that this still isn't a form of variance for the lifetime parameter of the
trait. This fails for example, because you can't coerce from dyn Look<'static>
to dyn Look<'w>
:
#![allow(unused)] fn main() { trait Look<'s> { fn method(&self, s: &'s str); } fn as_static(bx: Box<dyn for<'any> Look<'any>>) -> Box<dyn Look<'static>> { bx } fn as_whatever<'w>(bx: Box<dyn for<'any> Look<'any>>) -> Box<dyn Look<'w>> { as_static(bx) } }
As a supertype coercion, going from higher-ranked to non-higher-ranked can apply even in a covariant nested context, just like non-higher-ranked supertype coercions:
#![allow(unused)] fn main() { trait Look<'s> {} fn foo<'l: 's, 's, 'p>( v: Vec<Box<dyn for<'any> Look<'any> + 'l>> ) -> Vec<Box<dyn Look<'p> + 's>> { v } }
Fn
traits and fn
pointers
The Fn
traits (FnOnce
,
FnMut
,
and Fn
)
have special-cased syntax. For one, you write them out to look more like
a function, using (TypeOne, TypeTwo)
to list the input parameters and
-> ResultType
to list the associated type. But for another, elided
input lifetimes are sugar that introduces higher-ranked bindings.
For example, these two trait object types are the same:
#![allow(unused)] fn main() { fn identity(bx: Box<dyn Fn(&str)>) -> Box<dyn for<'any> Fn(&'any str)> { bx } }
This is similar to how elided lifetimes work for function declarations as well, and indeed, the same output lifetime elision rules also apply:
#![allow(unused)] fn main() { // The elided input lifetime becomes a higher-ranked lifetime // The elided output lifetime is the same as the single input lifetime // (underneath the binder) fn identity(bx: Box<dyn Fn(&str) -> &str>) -> Box<dyn for<'any> Fn(&'any str) -> &'any str> { bx } }
#![allow(unused)] fn main() { // Doesn't compile as what the output lifetime should be is // considered ambiguous fn ambiguous(bx: Box<dyn Fn(&str, &str) -> &str>) {} // Here's a possible fix, which is also an example of // multiple lifetimes in the binder fn first(bx: Box<dyn for<'a, 'b> Fn(&'a str, &'b str) -> &'a str>) {} }
Function pointers are another example of types which can be higher-ranked
in Rust. They have analogous syntax and sugar to function declarations
and the Fn
traits.
#![allow(unused)] fn main() { fn identity(fp: fn(&str) -> &str) -> for<'any> fn(&'any str) -> &'any str { fp } }
Syntactic inconsistencies
There are some inconsistencies around the syntax for function declarations,
function pointer types, and the Fn
traits involving the "names" of the
input arguments.
First of all, only function (method) declarations can make use of the
shorthand self
syntaxes for receivers, like &self
:
#![allow(unused)] fn main() { struct S; impl S { fn foo(&self) {} // ^^^^^ } }
This exception is pretty unsurprising as the Self
alias only exists
within those implementation blocks.
Each non-self
argument in a function declaration is an
irrefutable pattern
followed by a type annotation. It is an error to leave out the pattern;
if you don't use the argument (and thus don't need to name it), you
still need to use at least the wildcard pattern.
#![allow(unused)] fn main() { fn this_works(_: i32) {} fn this_fails(i32) {} }
There is an accidental exception to this rule, but it was removed in Edition 2018 and thus is only available on Edition 2015.
In contrast, each argument in a function pointer can be
- An identifier followed by a type annotation (
i: i32
) _
followed by a type annotation (_: i32
)- Just a type name (
i32
)
So these all work:
#![allow(unused)] fn main() { let _: fn(i32) = |_| {}; let _: fn(i: i32) = |_| {}; let _: fn(_: i32) = |_| {}; }
But actual patterns are not allowed:
#![allow(unused)] fn main() { let _: fn(&i: &i32) = |_| {}; }
The idiomatic form is to just use the type name.
It's also allowed to have colliding names in function pointer arguments, but this is a property of having no function body -- so it's also possible in a trait method declaration, for example. It is also related to the Edition 2015 exception for anonymous function arguments mentioned above, and may be deprecated eventually.
#![allow(unused)] fn main() { trait Trait { fn silly(a: u32, a: i32); } let _: fn(a: u32, a: i32) = |_, _| {}; }
Finally, each argument in the Fn
traits can only be a type name:
no identifiers, _
, or patterns allowed.
#![allow(unused)] fn main() { // None of these compile let _: Box<dyn Fn(i: i32)> = Box::new(|_| {}); let _: Box<dyn Fn(_: i32)> = Box::new(|_| {}); let _: Box<dyn Fn(&_: &i32)> = Box::new(|_| {}); }
Why the differences? One reason is that
patterns are grammatically incompatible with anonymous arguments,
apparently.
I'm uncertain as to why identifiers are accepted on function pointers,
however, or more generally why the Fn
sugar is inconsistent with
function pointer types. But the simplest explanation is that function
pointers existed first with nameable parameters for whatever reason,
whereas the Fn
sugar is for trait input type parameters which also
do not have names.
Higher-ranked trait bounds
You can also apply higher-ranked trait bounds (HRTBs) to generic type parameters, using the same syntax:
#![allow(unused)] fn main() { trait Look<'s> { fn method(&self, s: &'s str); } fn box_it_up<'t, T>(t: T) -> Box<dyn for<'any> Look<'any> + 't> where T: for<'any> Look<'any> + 't, { Box::new(t) } }
The sugar for Fn
like traits applies here as well. You've probably
already seen bounds like this on methods that take closures:
#![allow(unused)] fn main() { struct S; impl S { fn map<'s, F, R>(&'s self, mut f: F) -> impl Iterator<Item = R> + 's where F: FnMut(&[i32]) -> R + 's { // This part isn't the point ;-) [].into_iter().map(f) } } }
That bound is actually F: for<'x> FnMut(&'x [i32]) -> R + 's
.
That's all about higher-ranked types for now
Hopefully this has given you a decent overview of higher-ranked
types, HRTBs, and how they relate to the Fn
traits. There
are a lot more details and nuances to those topics and related
concepts such as closures, as you might imagine. However, an
exploration of those topics deserves its own dedicated guide, so
we won't see too much more about higher-ranked types in this
tour of dyn Trait
.