I try to get an overview of the variance behaviour of ATs and GATs but I can't find it anywhere well documented. Could anybody help me?
What I know (or think to know) so far:
GATs respectively their paramters are invariant in general.
At ATs it depends:
- When it is eg in a generic trait and you access it via projection like Trait::<'a>::At it's invariant.
- When you can normalize it, it may have a different form of variance.
Is that correct? Are there any other cases and/or is there any rule of thumb?
I thought about the topic again for a few hours and tried to build a mental model for myself. I would be super happy if you could check the following sentence for correctness:
Traits, trait impls, GATs, opaque types and other constructs for which no concrete type has yet been determined generally behave invariant in respect to their parameters or captures, since there is the possibility that they will be normalized to a type that can have more or less arbitrary variance properties and invariance therefore represents the lowest common denominator before normalization.
Yeah, that's generally how I think about it. And there's no trait bound or similar to declare or assert the variance of a particular parameter.
Opaque types do have a type known to the compiler, and also leak auto traits similar to how structs do, so for awhile I had a disconnect in my mental model about that. I.e. why don't they leak variance like structs do too?
In a comment linked here,[1] Niko suggest thinking about them more as unnormalized trait projections, by which I mean:
trait YourOpaque<'stuff, Captured> {
// This is not normalizable
type TheTy;
}
fn elsewhere<T>(_: &str, _: T) -> <() as YourOpaque<'_, T>>::TheTy {
..
}
For my mental model anyway, this way of thinking about it removes the disconnect about invariance (but makes the leaking of auto traits exceptional instead).
I think with auto-trait leakage you mean what Jon Gjengset is talking about here, right?
Seeing opaque types as unnormalized trait projections contradicts this indeed, I agree.
Well, I suspect that there are some things even in Rust (as in every other programming language) that are not logically derivable implementation details that you just have to know.^^
Right. Incidentally, the leakage of auto traits isn't just because it's less to type; that's not the important "nicer to work with" property. It's because it allows for an opaque type to conditionally implement those auto-traits, based on the captured parameters.
// Does the returned type implement `Send`?
// It does if and only if `T` does!
fn ex<T>(t: T) -> impl Sized { t }
// This fails because the `Send` bound only holds if `T: Send`
// and we didn't require `T: Send` on the inputs
fn ex<T>(t: T) -> impl Sized + Send { t }
// This works but is less flexible than just not mentioning `Send`,
// because you can't call it if `T: Send` doesn't hold
fn ex<T: Send>(t: T) -> impl Sized + Send { t }
(This is very related to why impl Trait + 'lifetime often is not what you actually want.)
I think there's an interest in being able to annotate these conditional scenarios, but I don't know where progress is offhand. But anyway, yeah, opaque types remain magical in some ways; there is no 1-to-1 mapping to something else.
// Does the returned type implement `Send`?
// It does if and only if `T` does!
fn ex<T>(t: T) -> impl Sized { t }
Hmm.. but if T does not implement Send but has a function which can return something that does?
(This is very related to why impl Trait + 'lifetime often is not what you actually want.)
Would you be so kind as to help me out here? The only pitfall regarding impl Trait + 'lifetime I know is that it means at least as long as 'a and that's mostly not what you want therefore you should write something with an empty helper trait like impl Sized + Captures<&'a ()>. Is this linked to that problem somehow?
Right -- impl Trait + 'a imposes a : 'a bound on the return type, which isn't desired if it captures other things like generic types or other lifetimes that could possibly have a shorter validity than 'a. It imposes an undesired constraint on everything that's captures. If you use use Captures<&'a ()>, then the return type is tied to that lifetime, but doesn't have to be valid for the entire lifetime.
The return (&'a str, &'b str) captures both 'a and 'b, but it isn't necessarily valid for all of 'a and it doesn't have to be valid for all of 'b either. It's valid for the intersection of 'a and 'b.[1]
And in the playground above, impl Sized + Sync imposes a Sync bound on the return type, which isn't required if it could possibly capture something that's not Sync. It's also imposing an undesired constraint. It's not the exact same scenario because the same workarounds don't apply, but it's related in that impl ... + MoreStuff imposes a bound on the entire return type, and that's more restrictive than we want.
If we're talking about inferred function body lifetimes, it's possible neither of 'a: 'b or 'b: 'a is true, too. ↩︎