I've on-again and off-again picked up Rust tutorials for the past 7 or so years but haven't had an opportunity to use it for either my job or any major projects yet. In the 10 or so times I've picked up the language and built a silly little Fibonacci calculator or other tool for fun, I always get tripped up by the same issue. The language requires me to provide explicit lifetimes even when, as far as I can tell, they should be obvious!
I'll pull an example from The Rust Book:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
In this function definition it's unclear whether the reference returned by the function will have the same lifetime as x
or the same lifetime as y
. In fact, you literally can't know -- it could be either!
But here's what we do know: the returned reference will live at least as long as the shorter-lived of the two! So even without declaring any lifetimes, I know the following:
fn main() {
let string1 = String::from("abcd");
let result;
{
let string2 = String::from("xyz");
result = longest(string1, string2);
println!("{}", result); // both string1 and string2 are still in scope, so this will ALWAYS work
}
println!("{}", result); // string2 is out of scope, so this MIGHT fail (unknown)
}
For single-input functions Rust already has a lifetime elision rule to handle this. But for multi-input functions, a decision was made to require explicit lifetimes. This feels like extraneous work for developers.
If you always assume the shorter lifetime of the two inputs is the lifetime of the result, then you can never run into a situation where the borrow checker fails to catch an error.
Doing lifetime elision like this also wouldn't preclude developers from choosing to override the default lifetimes when they know better. For instance:
fn one_char<'a>(a: &'a str, b: &str) -> &'a str {
if b == "first" {
return &a[0..0];
} else {
return &a[1..1];
}
}
fn main() {
let input = String::from("test"); // lifetime 'a
let result;
{
let decider = String::from("first");
result = one_char(input, decider);
} // decider falls out of scope, but the borrow checker knows result has a lifetime of 'a
println!("{}", result); // no errors
}
I've tried Googling but can't find a good justification for why "assume the shortest lifetime" elision like this isn't done. I also tried asking ChatGPT but it went in circles reiterating the "longest" example from the Rust Book but never actually explaining the reasoning.