Anotating lifetimes manually

The book THE RUST PROGRAMMING LANGUAGE provides the example below, then states that Rusts borrow checker should ". . .reject any values that don't adhere to these constraints."
Would this mean that if Rust detects that if a lifetime of a particular variable is not long enough to fulfill it's need in the code below, that Rust would not compile or is it that Rust will adopt the the longest of the lifetimes of the arguments provided to the function and ensure that the lifetimes are sufficient to complete the function?

fn main()
{
    let string1 = String::from("abcd");
    let string2 = "xyz";
    
    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}




fn longest<'a>(x: &'a str, y:&'a str) -> &'a str
{
    if x.len() > y.len() {x}
    else {y}
}

I’d say it’s hard to answer your question because it’s not really clear what you’re asking. Naturally it’s hard for a beginner to find the right words for asking questions, but perhaps if you just try to explain in more detail what the two alternatives you’re asking about would look like / work like, it might help :wink:

It is certainly also the case that the borrow checker can have the effect that a Rust program does not compile; it is also possible that Rust “wisely” chooses appropriate lifetimes for borrows, or lifetime arguments for function, in order to make your code works and the chosen lifetimes fulfill all required conditions.

How exactly the borrow checker operates in details is pretty complex; the main goal of this explanation in the book is that you understand the meaning of the lifetime annotations in the function signature, not to get a good overview of how the borrow checker itself works. This is important, because function signatures with incorrect lifetime annotation are almost a guarantee to run into borrow checking errors, either in the definition of the function (if the lifetimes in the function signature are too general) or at the call-site (if the lifetimes in the function signature are too restrictive). If the function signatures are all correct, then within each single function, the borrow checker will present itself as being quite smart and needing no extra “help” from you anyways, and often when it complains there will be an actual problem with your code. (For identifying such “actual problems”, it’s of course also important to understand the basic rules of borrowing in Rust, i.e. things like &mut … references are exclusive, references can’t exist longer than the thing they’re referencing exists, etc.

3 Likes

First let me highlight that the function signature is a two-way contract. The writer of the function is telling consumers of the function what it does (and doesn't do) -- consumers of the function can rely on the contract, like what arguments they can pass and the return type. The compiler will also make sure that the body of your function actually does fulfill the contract. If your function body doesn't do what the signature says it can, you'll get a compiler error. Consumers of the function also have to obey the contract -- in this case, they have to be able to borrow a couple str to pass into the function, for example.

In particular, note the contract doesn't change based on the body of the function [1], or on what exact arguments you passed it. The flow of borrows indicated by the lifetime parameters is part of the contract, and Rust isn't going to change the meaning of the contract by looking at the body of your function or considering which arguments were passed in at every call site.

When you have an elided lifetime, or a lifetime with no further bounds like the 'a in your post, that means that the function must be able to handle any lifetime at all, no matter how long or short [2].


With that out of the way, were you asking what happens if you removed the annotations?

Let's just try a few things and just see what happens.

Here I just removed the annotations:

fn longest(x: &str, y: &str) -> &str { /* ... */ }
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

What's going on here is that the contract is considered too vague -- the lifetime on the return value could be 'static, or it could be related to x specifically, or it could be related to y specifically, or it could be related to either of x or y. The language could define a rule that says which of these is the default, but it is considered too arbitrary for this particular case. There are other rules where it's considered intuitive enough to not write out the lifetimes -- see this explanation of lifetime elision in function and method signatures.

Here I ignore the hint and add 'static to the return value (only) to get rid of the error that said the return value needed a named lifetime.

fn longest(x: &str, y: &str) -> &'static str {
error[E0312]: lifetime of reference outlives lifetime of borrowed content...
  --> src/main.rs:11:9
   |
11 |         x
   |         ^
   |
   = note: ...the reference is valid for the static lifetime...
note: ...but the borrowed content is only valid for the anonymous lifetime defined here
  --> src/main.rs:9:15
   |
9  | fn longest(x: &str, y: &str) -> &'static str {
   |               ^^^^

[Same error again but for `y`]

The contract is unambiguous now, but the function body can't fulfill it. You're returning x or y and you can't grow their lifetimes to be 'static. Remember, their (elided) lifetimes could be arbitrarily short, and you have to handle every possible lifetime! This return type could only work with the given function body if x and y also had the 'static lifetime.

Here I've added a lifetime parameter to return instead, but only put it on x.

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
error[E0621]: explicit lifetime required in the type of `y`
  --> src/main.rs:13:9
   |
9  | fn longest<'a>(x: &'a str, y: &str) -> &'a str {
   |                               ---- help: add explicit lifetime `'a` to the type of `y`: `&'a str`
...
13 |         y
   |         ^ lifetime `'a` required

This is similar to before, but the error is quite different. Everything works out where you return x, but when you return y, you might be returning something with a lifetime shorter than 'a. Again, y's elided lifetime could be anything at all, including something shorter than 'a (which can also be any lifetime at all). It would be invalid to make the borrow longer, and returning something shorter than 'a would not meet the contract. For the function body to work, you need y's lifetime to be at least as long as 'a.

This version has the same meaning with regards to lifetimes, it's just more explicit.

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
error[E0623]: lifetime mismatch
  --> src/main.rs:13:9
   |
9  | fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
   |                                   -------     -------
   |                                   |
   |                                   this parameter and the return type are declared with different lifetimes...
...
13 |         y
   |         ^ ...but data from `y` is returned here

And finally, one more with a syntax you may not have seen yet:

fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
//               ^^^^

'b: 'a means "'b lasts at least as long as 'a" or "'b is valid for 'a", and is often phrased "'b outlives 'a" (but note that 'b and 'a could actually be the same).

In other words, this is the requirement I mentioned above: y's lifetime 'b is at least as long as 'a.

This version compiles, because it is ok to shrink the lifetime of &'b str to be &'a str here. So this signature says "I'll return something with the shorter lifetime of the two arguments (which is the lifetime on x)." But note that doesn't mean it always returns x! It could return any &str with a lifetime as long as 'a, and that includes y, and it includes &'static strs as well. More on this in a second.

Now, this may seem more flexible than the version where both x and y have lifetime 'a, since you can call it with different lifetime on y. But it practice it doesn't matter, because Rust will typically just pick the shortest lifetime needed for all the parameters when you call the function [3] -- so in this case, 'b is almost always going to be 'a anyway. And that's also what you want -- there's no reason to borrow y for a long time since you can only use the returned value for 'a anyway.

Returning back to the original:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

This says "give me two &str with the same lifetime, and I'll return a &str with that same lifetime." Again, this probably means you're returning x or y, but you could actually return anything that lasts at least as long as 'a, like a &'static str:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        // This is a `&'static str`
        "Don't ask me, I'm busy"
    } else {
        y
    }
}

Does this mean that at the call site, x doesn't need to be borrowed, because it's never returned? Or that it can be borrowed for shorter than y? No, because the contract [4] says it's possible for either x or y to be returned, so they must be borrowed in order to call the function; the caller doesn't get to assume x isn't returned. The contract works both ways: if you can't borrow x for whatever reason, you can't call the function, even if the current method body doesn't need the borrow of x.


These "contracts" work this way so that you can't break downstream code by just changing the body of your function. You can do anything within the bounds of the contract in the body of the function, and callers don't get to assume any details of your implementation beyond the contract of the function. However, callers can also call you in any way within the bounds of the contract, e.g. call the function with any lifetimes whatsoever, and the function body must always respect that too (or the compiler will error).


  1. except in advanced cases you shouldn't worry about for now ↩︎

  2. well okay, the lifetime does last for the length of the function itself ↩︎

  3. or at least, that's a mental model that generally works ↩︎

  4. the function signature ↩︎

12 Likes

Dude, that was a lot of work! thx