Lifetime elision Alteration

New user here. While I was going through the book I ran across chapter 10.3 and the example they gave that failed the Lifetime Elision rules.

Namely this function:

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

Having read a similar, but unfortunatly poorly worded request. I'd like to reopen this discussion and touch on how my suggestion differs from the thoughts brought up by it's author.

Firstly he suggests that the above function should be desugared to:

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

Which fortunately is what rustc literally suggests (and as such what vsCode will quick fix to).

He further build upon this and specified that each unique type should gain it's own lifetime.

fn first_match<'a, 'b>(haystack: &'a str, regex: &'b Regex) -> &'a str;
fn first_match<'a, 'b>(haystack: &'a str, regex: &'b Regex) -> &'b Regex;

I'd like to step away from this case and the complexity it brings up in favor of this:

All unspecified lifetimes will be given the same lifetime.

or:

fn first_match(haystack: &str, regex: &Regex) -> &str;
//would desugar to:
fn first_match<'a>(haystack: &'a str, regex: &'a Regex) -> &'a str;

This case is the only safe case that the compiler could hope to assume. (The thread I linked mentioned how appling lifetimes based on type is wildly complex).

Would this break any existing code? No, as this 'new' rule would have to be applied after the rule for methods. Any code that it would allow to compile currently doesn't, and any code that does should compile the same.

This example here the function would still fail. However, that is fine. We can teach new users about lifetimes not by breaking ambiguous functions, but rather throwing an actual lifetime error. If you remove the 'static lifetime from the return from example_use1, it would still fail to compile since we're trying to return a lifetime that must end at the end of the function block. rustc could then call out that the lifetime of the return ends too soon. Which much better teaches the purpose of lifetime notation for returns, telling the compiler when the return should die, than disallowing multiple borrowed arguments without specifying the lifetime.

Because there is nothing wrong with the longest function from the book. Sure it doesn't currently compile, so in that sense there is something wrong with it. But sure, it will cause the lifetime of the regex passed to first_match to end much sooner than potentially expected. But when the user is trying to use that regex passed it's lifetime, that is the time to teach the user about lifetimes. Not when they're trying to write the function in the first place.

If someone is writing code and is going through the process of testing and verifying their code, let them get a layer or two into a call stack and verify their code is working, then when they try to use the value passed from a higher level down past the point a different agrument expires it's time to learn about lifetimes. Even for that first_match function, we could call out something like this:

help: consider introducing a named lifetime parameter
 |
9| fn first_match(haystack: &str, regex: &'a Regex) -> &str {
 |                                        ++

There is enough information to determine this. With the value that the user is trying to use past it's lifetime we could tell the user that hey, maybe that value needs a specified lifetime. The book's example will need to be changed, as it's example would start compiling with this change. Instead we'd need one that would break with this rule.

If you suggest that this rule will only apply to references, then I don't see value in it for the those few cases.

If you suggest it will apply to any lifetime, it is a breaking change.

In any case, I disagree with this proposal because this is not always the correct rule to apply. Sometimes the return type borrow from only one argument and if you'll apply this rule the result will be more restrictive than necessary and might cause confusing errors later, while if you error hopefully the programmer will stop and think. Admitted, this can already happen with the current elision rules, but I believe this is more rare.

3 Likes

It would also effectively break anything with elided nested lifetimes, especially if invariance is involved.

If we were to add such an elision rule, it should probably only apply/unify when all elided lifetimes are covariant.

First of all, I must be missing something, but I fail to see why &'a mut &'a str breaks. It obviously does, but why? Reading the referenced error code even changing it to &'a &'b should still fail. But it doesn't? Bizzar.

Also, just like the current rules, it's not supposed to always be right. It's supposed to handle most of the cases. The perfect fix is a unicorn.

But perhaps a better solution is to give each layer it's own lifetime that's equal or greater than the layer above. So && -> &'a &'b where 'b : 'a.

Because &mut T is invariant in T, so the inner reference is exclusively borrowed for the rest of its validity.

1 Like