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.