Variance at ATs and GATs

Hey folks,

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?

Regards
keks

  • Trait parameters are invariant
  • GAT parameters are invariant
  • Non-generic ATs don't have parameters
  • If an AT or a GAT can be normalized, the normalized type has its usual variance

I have seen a number of errors around normalization not kicking in and the like; feel free to ask about more specific code.

  • (You didn't ask but) opaque type and lifetime captures are also invariant
3 Likes

Thank you so much for your fast and precise answer! :slight_smile: :medal_sports: That sums it up perfectly and is exactly what I wanted!

1 Like

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.

Is this about right?

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).


  1. "parameterized opaque types are more like a GAT or a parameterized trait with an associated type" ↩︎

1 Like

Thanks for the verification! :slight_smile:

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.

1 Like
// 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? :smiley: 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?

I don't understand your question but maybe this helps.

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.

Here's an example that doesn't use impl Trait.

fn two_lifetimes<'a, 'b>(a: &'a str, b: &'b str) -> (&'a str, &'b str) {
    (a, b)
}

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.


  1. If we're talking about inferred function body lifetimes, it's possible neither of 'a: 'b or 'b: 'a is true, too. ↩︎

1 Like

I don't understand your question but maybe this helps.

Ahhh forget about that, you're right of course. I was missing the forest through the trees. :smiley:

Thanks for the examples I got it now. :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.