Question about invariance for &mut T

Hello!
I've been watching jonhoo's Crust of Rust for Subtyping and Variance and I didn't get one thing.
I don't understand why &mut T is invariant and not contravariant over T. Even the example I came up with works:

fn foo<'a>(a: &mut &'a str, b: &'a str) {
    *a = b;
}

fn check_static(_s: &'static str){}

fn main() {

    let a = "abc".to_string();

    let b:&'static str = "abc";
    check_static(b);

    let r: &mut &str = &mut a.as_str();

    // r should be &mut &'a str
    // b should be &'static str
    //
    // since &'a mut T is invatiant over T &'a str should equal to &'static str
    // for this function to be callable. I don't understand why I can still call this.
    foo(r, b);
}

Your example follows the sub-typing and variance rules exactly;

  • r is &mut &str. It is passed to an a of required type &mut &'a str. Since &mut T is invariant over T, compiler concludes that r must in-fact be &mut &'a str (making T of type &'a str).
  • b is of type &'static str. It is passed to a b of required type &'a str. Now, 'static is a subtype of any 'a. Hence, compiler can pass a &'static str where a &'a str is expected.
4 Likes

ah. That makes sense. Now I don't understand why this function signature also works:

fn foo<'a>(a: &mut &'a str, b: &'static str) {
    *a = b;
}

Isn't &'a str supposed to be exactly equal to &'static str(because of invariance not covariance)? Or is 'static downgraded to 'a here as well?

AFAIK it's not a matter of variance here. Here, the compiler checks if b has a lifetime that is compatible with *a. Since 'static is compatible with any 'a (compatible in the sense of sub-typing, 'static is a subtype of any 'a), the compiler allows it.

Here's an example I wrote for another recent thread where memory unsafety is demonstrated when forcing a &mut T to be either covariant or contravariant over T (either expanding or contracting the lifetime underneath the &mut).

(Edit: Then I added more that's not entirely correct, but I have to leave for the moment, so I'll just remove it for now.)

4 Likes

OK, here's the other part.

Here:

fn foo<'a>(a: &mut &'a str, b: &'static str) {
    *a = b;
}

Nothing about b's type is invariant -- the lifetime is covariant. And in fact, any time you're making an assignment to *a, you must have gotten ahold of a &'lt str, and those are covariant over their lifetime. So in this case, the &'static can shrink to a &'a.

Furthermore, consider:

pub fn bar<'a>(a: &mut &'a str, b: &mut &'static str) {
    *a = *b;
    let _c: &'static str = *b;
}

Here, the type of b is invariant in the inner lifetime -- the 'static. You can't do this for example:

let local = String::new();
*b = &local; // would require 'static to be covariant (contract)
             // or &'local to be contravariant (expand)

Because the &local is covariant and the 'static is invariant. However, because shared references implement Copy, you can copy the &'static str out from behind the &mut of b. Once it's copied out, you have a &str type again, and it's covariant. (_c more directly illustrates the ability to do the copy. Note that &mut references are not Copy.)

Variance is what determines which types are or are not subtypes of other types -- the compatibility. 'static is a subtype of any 'a in covariant context, but it's the other way around in contravariant context (and it's only a subtype of itself in invariant context).

4 Likes

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.