Lifetime shortening and turbofish ascription

I ran across this in someone else's thread but didn't want to derail that discussion.

Rust allows this:

fn insert_str<'a>(map: &mut HashMap<&'a str, usize>, key: &'a str) {
    map.insert(key, 0);
}

fn main() {
    let mut map = HashMap::<&'static str, usize>::new();
    let key = String::from("3");
    insert_str(&mut map, &key);
}

I.e. the &'static str in the turbofish was shortened to &'_ str for the actual type. Playground.

Using type ascription on the binding itself does not:

    // Lifetime error, as expected
    let mut map: HashMap::<&'static str, usize> = HashMap::new();

The turbofish behaviour surprised me. In the process of writing this post, though, I believe I've come to understand it:

  • Compiler sees HashMap::<&'static str, usize>::new() and resolves it to an implementation that does not involve an explicit 'static lifetime
  • The binding in turn does not have a 'static bound
  • Nothing else requires a 'static bound either
  • So the type of map has a resolution (which is not 'static) and the program compiles

And nothing unintentional (language wise) is going on. Is this all correct? I guess this means turbofish has variance?

3 Likes

This is because of variance. The type of map is not actually HashMap<&'static str, usize>. If you really wanted to enforce that, you should use type ascription.

Because

  • HashMap is covariant in both its arguments
  • &'a T is covariant in both its arguments

we can deduce that
HashMap<&'a _, _> is covariant in 'a. In particular, this means that HashMap<&'static T, U> is a subtype of HashMap<&'_ T, U>. So it is always fine to assign HashMap<&'static T, U> to HashMap<&'_ T, U>.

Then inference continues on and finds that insert_str() enforces that 'a must be 'key (lifetime of key)1, and then adds the constraint that 'a: 'key. Then inference ends, and sees that 'a = 'key (because there are no other constraints). We then know that

So it is always fine to assign HashMap<&'static T, U> to HashMap<&'_ T, U>.

So the first assignment is fine, and in the end map: HashMap<&'key str, usize>

Variance hides everywhere, even where you wouldn't first think to look :slight_smile:

1 how it knows this is also because of variance, we need to add in one more rule:

  • &mut T is invaraint in T

Then we can see that &mut HashMap<&'a _, _> is invariant in 'a, so the lifetimes of the arguments of insert_str must match exactly.

Read more about subtyping and variance in the nomicon

7 Likes

Thanks @RustyYato, thorough as always!

Playing with it.

2 Likes

Though it is important to note that the other argument has the type &'a str, so really the key's lifetime doesn't have to match exactly — it may be longer.

3 Likes

Variance strikes again!