Building an intuition

Consider this somewhat contrived function that takes a static string and makes its lifetime shorter:

#![allow(dead_code)]
fn main() {}

fn lifetime_shortener<'a>(s: &'static str) -> &'a str {
    s
}

Intuitively, this feels like it should compile: if a string lasts for the whole process it should also last for any part of it. And it does!

Now let's make it a bit more complicated. Consider a mutable reference to a HashSet.

#![allow(dead_code)]
fn main() {}

use std::collections::HashSet;

fn hash_set_shortener<'a, 'b>(
    s: &'a mut HashSet<&'static str>,
) -> &'a mut HashSet<&'b str> {
    s
}

hash_set_shortener doesn't compile!

Can you tell why? Think about it for a minute, try using your intuition...

#![allow(dead_code)]
fn main() {}

use std::collections::HashSet;
use std::iter::FromIterator;

fn hash_set_example() {
    // Consider this HashSet over static strings.
    let mut my_set: HashSet<&'static str> = HashSet::from_iter(["static"]);

    // Do you think this can work?
    let owned_string: String = "non_static".to_owned();
    my_set.insert(&owned_string);

    // Doesn't seem like it can, right? my_set promises that the &strs inside it
    // are all 'static, but we tried to put in an owned string scoped to this
    // function.
}

As a counterexample:

#![allow(dead_code)]
fn main() {}

use std::{collections::HashSet, iter::FromIterator};

fn hash_set_shortener<'a, 'b>(s: &'a mut HashSet<&'static str>) -> &'a mut HashSet<&'b str> {
    s
}

fn hash_set_counterexample() {
    let mut my_set: HashSet<&'static str> = HashSet::from_iter(["static"]);
    let owned_string: String = "non_static".to_owned();

    // If we pretend that hash_set_shortener works...
    let shorter_set = hash_set_shortener(&mut my_set);

    // then you could use `shorter_set` to insert a non-static string:
    shorter_set.insert(&owned_string);

    // Now we can drop `shorter_set` to regain the ability to use `my_set`:
    std::mem::drop(shorter_set);

    // And my_set now has a non-static string in it. Whoops!
}

It isn't just &mut which is problematic in this way. This also occurs with any sort of interior mutability, like RefCell, OnceCell, or Mutex -- anything inside some sort of mutable context has this issue.

Now, what about a hypothetical "lengthener" function?

#![allow(dead_code)]
fn main() {}

fn lifetime_lengthener<'a>(s: &'a str) -> &'static str {
    s
}

This is clearly bogus, right? You can't just turn an arbitrary borrowed string and make it last the duration of the entire process. Similarly:

#![allow(dead_code)]
fn main() {}

use std::collections::HashSet;

fn hash_set_lengthener<'a, 'b>(
    s: &'a mut HashSet<&'b str>,
) -> &'a mut HashSet<&'static str> {
    s
}

But what about this? fn is a pointer to a function that takes an arbitrary borrowed string.

#![allow(dead_code)]
fn main() {}

fn fn_ptr_lengthener<'a>(f: fn(&'a str) -> ()) -> fn(&'static str) -> () {
    f
}

This feels like should work. You can take a callback that takes an arbitrary borrowed string and turn it into one that takes in a static string, since you're weakening the guarantee. And it does.

How can we handle these different cases in a principled way? That's where variance comes in. We're going to talk about this in the next chapter, Formalizing variance.