To understand the problem with this function signature:
pub fn calculate_risk_score(a: i32, b: i32) -> Result<i32, &str> {
todo!()
}
...consider what &str
means. It means: a borrowed slice of somebody's string. If such a function were allowed, one could call it like this:
if let Err(e) = calculate_risk_score(foo, bar) {
// `e` is &str - who is it borrowed from, and how long is it valid?
// Can I put it in a container? Can I return it from this function?
// Can I put it in a global variable?
...
}
Lifetimes provide answers to these questions by connecting the lifetime of the returned borrow to the lifetime of a function parameter, which is provided by the caller, so its scope must be known. This is often as simple as the scope of self
:
struct Parser {
last_error: String,
// ...
}
impl Parser {
// Equivalent to fn calculate_risk_scope<'a>(&'a self, a: i32, b: i32) -> Result<i32, &'a str>
fn calculate_risk_scope(&self, a: i32, b: i32) -> Result<i32, &str> {
if error {
return Err(self.last_error.as_str());
}
...
}
}
Now the lifetime of the &str
borrow is tied to the lifetime of the borrow of &self
, i.e. the returned error may be used as long as the object on which the method was invoked is alive and unmodified.
In case of the original function, &str
is the only reference in the whole signature, so there is no other borrow to connect it to, and the compiler requires you to supply an explicit lifetime. It suggests 'static
as a reasonable choice given the constraints, and it indeed works if you're using literal strings which are baked into the executable and never deallocated.
Note that the compiler will never use the body of the function to infer the lifetime of arguments or return values. This is intentional, and allows the signature of the function to constitute an API contract that doesn't depend on the implementation. When the function is simple enough that it receives one reference and returns one reference, the compiler will take it to mean that their lifetimes are connected, as if they were spelled out as fn calculate_risk_scope<'a>(&'a self, a: i32, b: i32) -> Result<i32, &'a str>
. This is called lifetime elision and works just by examining the function signature, completely ignoring its body. (The body must of course conform to the lifetimes in the signature.)