In current Rust, lifetimes are sometimes redundant. For example, in the following code, the compiler should be able to automatically infer that s1, s2 and the return value in longest have the same lifetime.
fn longest(s1: &str, s2: &str) -> &str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let string1 = String::from("Rust");
let string2 = String::from("Programming");
let result = longest(&string1, &string2);
println!("The longest string is: {}", result);
}
However currently it has to be specified manually:
Yea, the current lifetime reduces the design burden on the compiler. But on the other hand, based on the experience from Rust developers around, lifetimes bring a significant burden on users.
This is not very friendly to expanding the use of this language, especially for who want to write large projects with Rust. The tcx that is everywhere in the compiler's code is an example.
So I wonder if we can shift part of the user's burden to the compiler's automatic inference.
Not being able to tell what the lifetime relationships are based on the function signature is a deteriment to any Rust developer other than the developer actively writing the function --- including the same developer one month later. Having to read the function body in able to know try to infer the lifetime relationships is a burden on the developer.
An IDE being able to infer the relationships and supply the function signature for the actively writing developer -- that's a different story. That is where the desired functionality belongs.
According to this logic, the lifetimes should be like comments. We encourage everyone to write it, but we can relax the restrictions so that learners can learn, try and write the code faster.
While allowing inferred lifetimes may help learners in the simplest cases, imo it would just move the difficulty further down the line. In more complicated cases where the compiler can't infer the correct lifetimes (I assume there would be such cases), having not learned how lifetimes work in simpler cases, beginners might have an even harder time fixing the problem.
Having an IDE code action insert the lifetime annotations (as @quinedot suggested) would be a better solution for learners of the language too, because they could see what they should've written to make the code work, which makes them more likely to learn how lifetime annotations work[1] before hitting the harder cases.
than not having to write lifetime annotations at all ↩︎
In fact, my ultimate goal is to have only a few special cases that require users to annotate lifetimes, and let the compiler infer most other cases. Like you said, if only a small number of cases can be simplified, then there is indeed little value.
However, I still feel that the original purpose of lifetimes is to reduce the burden on the compiler, rather than asking users to understand the lifetimes of variables via manual declarations. It has been more than ten years since Rust 1.0 was released, and I think the designers of the language and compiler should really re-cosider this feature.
Rustc already infers lifetimes based on the function signature according to a couple simple rules. (Lifetime elision - The Rust Reference) Inferring them based on the body is going to be a semver hazard as any change to a function body can accidentally cause the public api to change.
Yea, semver hazard is an important consideration. But letting the compiler infer (assuming it can) is no more restrictive than manual annotations, so when users need to change their code, they won't make the project more complicated than if they had manually annotated it.
I think you're missing the point here. The function signature forms a contract between the caller and the callee (function body). If you make the lifetimes less restrictive to the callee, that means more restrictive to the caller and vice versa.
If I have a function like this:
fn foo(s1: &str, s2: &str) -> &str {
s1
}
and someone writes a function like this:
fn main() {
let s1 = "s1".to_string();
let s2 = "s2".to_string();
let r = foo(&s1, &s2);
drop(s2);
println!("{r}");
}
having inference would mean that I can change the body of foo[1] to be:
Do nothing (that’s probably the best thing to do).
Assume that if there are multiple arguments with references, then references in the output must outlive all references passed as arguments (ie. add a shared 'anonymous to all references), possibly behind a #![output_must_outlive_all_arguments] switch used during development, just like #![unused_code] and the like.
Ensure that both rust-analyzer and rustc have a "fix-me" button that uses inference and modify the source code to add lifetime annotation accordingly.
I assume that a combination of 2 and 3 could slightly improve the developer experience, but I’m not even sure 2 would really be that useful.
(disabling syntax highlighting because the parser botched it)
I think this is a big improvement because the 'a doesn't really mean anything, it's just a placeholder for a generic parameter - but this function ought to not be generic! We just lack the syntax to specify that "the output will borrow either from s1 or from s2", which is what &'(s1, s2) str means.
And IMO this is probably more intuitive and teachable than describing the same thing using generics: when you write fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str you crucially are depending on the compiler to unify the lifetimes of s1 and s2, which means that 'a is the smaller (or intersection) of the corresponding unconstrained lifetimes, which is IMO very subtle until you have internalized this concept - and then suddenly it becomes second nature and you are puzzled why people are confused about it.
Note that while simpler and easier to understand (IMO), this syntax is not the same as inferring the lifetimes: you still need to explicitly state which parameters exactly the output can borrow.
But... perhaps we could infer lifetimes of private functions, under the rationale that if we change the body of the function it won't break any crate that depend on it. But even then, I think that there should be explicit syntax to infer it, like this:
And if you add a pub to this function the compiler would of course refuse to compile, but also print the lifetime it inferred so you can paste into your code - and maybe rust-analyzer could apply it with a code action.
... and I would like to point out that when you are reading the documentation of a public API, "which parameters exactly the output can borrow" is important information that you need to know, and you don't get to look at the body of the function to figure it out.
(Often you can, but only by clicking through the [source] link; and the author of the API might not have chosen to publish the source; and it can be substantially more difficult than just reading the annotations!)
I think that for private functions like that, what people often want is not an inferred signature at all, but a "macro function" that essentially just copies the code into the caller. So it doesn't need to infer a trait type, but can just do x + y without worrying about what those types are until it's used. And where you can conditionally move something but the move checker runs in the context of the caller, etc.