Why does rust need our help in lifetimes?

Hi Rustceans!

Right now Im learning rust and Im trying to understand why we should Help borrow checker to understand life times.

the only thing it needs to do is :
1.analyze the function and see which parameters have to potential of being returned and become the functions result
2. if there is not one -> show error
3. if there is only one -> that one should be it
4. if there are more than one -> those parameters and result should all have a similiar lifetime ( for example 'a )

plus: the place that the function is called can give borrow checker more evidence to see if that piece of code is safe or not

There are enough data for it to judje safety + Specifying life times is just verbose and ugly.

should I request this from rust developers or Im missing something?

2 Likes

Suppose we have a function whose signature, without lifetimes, is fn foo(a: &A, b: &B) -> &C

What are the necessary relationships between the lifetimes of each of the three borrowed values, and how does the function body constrain them?

That's why.

4 Likes

Please note that in trivial cases, Rust doesn't need help.

  • No outputs lifetimes needed.
  • Single input lifetime needed (used for all outputs).
  • &self/&mut self (its lifetime is used for all outputs, as doing so noticeably lowers number of lifetimes that need to be specified)

It's when things become more tricky like fn(&A, &B) -> &C Rust needs help, as it can be feasibly any of those - and there is no way of telling which one:

  • for <'a> fn(&'a A, &'a B) -> &'a C
  • for <'a, 'b> fn(&'a A, &'b B) -> &'a C
  • for <'a, 'b> fn(&'b A, &'a B) -> &'a C

(as well as 'static lifetime and 'a: 'b lifetimes, but those usually need to be explicitly declared as they are somewhat rare)

You could analyze function body, but Rust did an intentional decision to not do global type/lifetime inference to avoid code breaking due to change inside a function (Haskell has such an issue, in practice people declare types of everything that is exported to avoid such issues).

4 Likes

This is what finally got the need for lifetimes to click for me. Rust's function signatures act like barriers against unpredictability, and absolute promises about behavior. As soon as relationships require special reasoning about the internals of a function, all the promises go away. If I change the internal lifetime relationships of my function without being forced to also change its signature, than anyone calling my function is forced to dig through my source code in order to know what has changed. Similarly, if I refactor my code, a component which was not internally modified in any way can suddenly break or change its behavior.

I find it easier to think about explicit lifetimes, not so much as giving the compiler a hint, but as making a promise to the compiler and to all the code that calls my code. A fair price for fearless refactoring if I do say so myself :slight_smile:

12 Likes

wich one of a or b will be returned?

If you're suggesting that the return lifetime could be inferred, yes, that is certainly often true. But:

  • in some cases it isn't: e.g. when you're writing a trait definition, or when the function is doing something unsafe, and
  • more importantly, it would clash with Rust's choice of limiting type inference to function bodies. Just as you have to specify types for all arguments, you also have to specify lifetimes that are part of types. (Elision lets you type less, but has nothing to do with inference.)
3 Likes

Just to provide an example. You are a compiler, and see following code:

pub fn a_function(a: &str, b: &str) -> &str {
    ...
}

How would you handle lifetimes? Note that because type inference is by design not available for globals, you cannot read function code - doing so would cause a small change to a function body to possibly break codes that use this function, without crate author even noticing.

Now, there are two possible ways to deal with it.

  • Don't support lifetime elision for such functions (which is what Rust currently does ONLY for such functions).
  • Assume a_function<'a>(a: &'a str, b: &'a str) -> &'a str. This works, but it may be unnecessarily restrictive if this function doesn't need such a declaration, which may be annoying to user, and crate author may not know about an issue.

Note that the following aren't options, as there is no reason to prefer one argument over another.

  • a_function<'a, 'b>(a: &'a str, b: &'b str) -> &'a str
  • a_function<'a, 'b>(a: &'a str, b: &'b str) -> &'b str

Explicitly requiring specifying intended lifetime means that the author considered lifetimes of a function, they aren't unnecessarily restrictive, and that it is a part of stable API.

2 Likes

I view specification of lifetimes as basically the same thing as specifying types (or bounds on generic type parameters) - Rust is unique in that it adds that second dimension, so to speak, but otherwise it's just type information (if you squint a bit until you get comfortable with it).

3 Likes