-
Notifications
You must be signed in to change notification settings - Fork 3.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ch10-03 Explanation of lifetime bounds is confusing/incorrect #3235
Comments
Can you construct an example that illustrates this? |
The example was listed in the book itself: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
} Obviously the lifetime annotations work correctly in practice. But given the description in the book, they don't:
If |
No, can you give me an example of a |
I feel like we're talking past each other here. I can't construct that scenario for you in code, because such a scenario can't exist. That's because there's nothing wrong with lifetimes in terms of how they actually work, so any code example would work just fine (because the only way you could interpret it would be according to the actual rules that Rust uses.) What this bug is about is about the explanation of lifetimes, as given in the book. In other words, if we take the book's explanation as canonical for how lifetimes work, where does that lead, logically? And I'm saying that the explanation is flawed. The example I gave has "the lifetime of |
If you're still hung up on the "only until program start" bit, here's another way of looking at it:
|
It can't, though, because if it ends then you don't have a value to pass to the function. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b6cb1ecf171f98d721d8fcbfccbe98b6
results in
I don't understand why you're trying to understand the formal semantics of lifetimes by using situations that can't exist? |
The formal semantics of the meaning of the lifetime parameters is that they describe the relationship of the input parameters' lifetimes to the output's lifetime, as explained here:
I don't understand what you're looking for, exactly, and why this paragraph isn't it. |
This example doesn't show anything (directly) about the lifetime parameter
This gets to the heart of the issue. Lifetime parameters don't have any intrinsic meaning on their own; the only meaning they have is in relating the lifetimes of concrete references. So, it is perfectly sensible to talk about hypotheticals where " In the context of how Rust actually works, these hypothetical lifetimes would be impossible for |
Let's try a different direction, just with generic type parameters for a moment. Given this example:
This says the function accepts some type T as the parameter When you say:
It's akin to saying "let's talk about a hypothetical where the Lifetime parameters get filled in with concrete lifetimes when the functions are used, and those lifetimes must be valid ones. I wonder if maybe that's the problem? That the book doesn't specifically say in this spot that all references must be valid? (It's said elsewhere). |
What makes you say this, exactly? The lifetime parameter Making an analogy with generics again, it sounds like here you're saying because, say, |
I have an example of diagnostics that mention the value of lifetime parameters, here:
results in
|
I agree, let's try a different direction. What does this sentence (from the book) mean, to you?
I think we might be closing in on the confusion with your example of generic types, though. You seem to think generic types and lifetime parameters work the same way. I don't. I suspect you're correct (since I'm new to Rust), so let me try to explain how I see the difference, which comes from the explanation in the book. fn something<T: Copy>(input: T) -> T {
// ...
} As you said, in this case the type parameter fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// ...
}
There is a crucial difference here: Similarly, right now the book says the return type will live at least as long as lifetime Does this clear things up? |
This still doesn't mention the value of lifetime parameters. It highlights that the lifetime parameters are different, but it doesn't say what their values are (i.e. what the actual lifetimes of those parameters are). This might seem like a minor quibble, but if I'm right about how lifetime parameters work it's essentially impossible for diagnostics to emit a concrete value for a lifetime parameter, because they don't actually have one. (I.e. they're only used to bound the lifetimes of concrete references, which do have concrete lifetimes.) |
Actually, I know lifetimes can't work the same as generics, because of this example: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
} If |
FWIW, I agree that those sentences are confusing. For me the more confusing sentence is:
Let's take this example (which doesn't compile due to a borrow checker error): fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
} It is explained later in the book that the concrete value of So the concrete lifetime of Now if that sentence is correct, then
|
Aha, I think I see a missing piece here. Lifetimes are similar to generics, but they aren't exactly the same, that's true. The
So take this example, listing 10-23:
When this Another way to say this is that
Ah, I was confused. I thought you were looking for diagnostics that used a literal There have been some projects attempting to provide visualization of lifetimes; as you can imagine, it gets complex. Here are some examples:
No, "at least" is correct and "at most" is incorrect. There's a long discussion in this PR, but the short story is that
|
This is confusing. I mean your interpretation is trivially true, why even mention it? Of course, the return value lives at least as long as its reference parameters... it's basically impossible to construct a function where this isn't true (afaik). At least I can't imagine how it would be possible. In And that sentence doesn't help the reader understand why the following code fails the borrow checking: fn main() {
let x: String = String::from("foo");
let y: String = String::from("bazz");
let z: &str = longest(&x, &y);
drop(x);
// borrow checker complains, z's lifetime exceeded 'a
dbg!(z);
} My understanding of the function signature is that the lifetime of the variable which gets assigned the return value ( Anyways, I'm giving my perspective as someone who is reading the book for the first time. I will try to read the thread you linked to. |
Well, I'm still not convinced... Tend to agree with @mulkieran |
There's a lot going on here. In Here are the lifetimes annotated in the example you provided:
So what the signature of I'm still not sure what to change about the book because I'm not going to change it to something incorrect. I am interested in making it more clear, but I'm not sure what that is yet. |
Yes! I feel like we're finally getting through to each other. Especially now that we're on the same page that generics and lifetimes don't work the same.
To be clear, I'm not looking to visualize or even get concrete values for the lifetime of What I'm after is understanding the rules for determining the constraints, because I haven't seen them specified anywhere. This bug is talking about how the rules that are mentioned, are definitely incomplete:
Specified formally: ...And that's it. That's all the rules we are given so far. You'll notice that there's no rules that bound the beginning of anything's lifetime, and there's no rules that bound the end of lifetimes from above. Without those rules, we (and the borrow checker) don't have enough information to rule programs as valid or invalid. I guess my point is, there are clearly additional (formal) rules, and I want to know what they are. :) |
How did you determine that |
Agree with @d0sboots comments.
Well, I think we both agree on that. My contention is that the book doesn't do a good job of explaining that. That sentence you wrote is already better in my opinion. Let's back up a bit. The point of lifetimes (and this section of the book) is to determine when it is safe to use a reference. As the caller of a function, a return lifetime In other words, even if the referenced value can live longer than That is why I found the current phrasing confusing. While it is saying technically true (e.g. yes the value of the return could live longer than
Those sentences are technically true. But when you read them for the first time, you are left to wonder: ok so what? This doesn't help me know when the reference will become invalid. The value can live beyond After all this discussion I now understand that I maintain that this should be rephrased, the sentence you wrote is already better IMO. |
It's explained a bit later in the chapter (perhaps not very formally) as being the lifetime where the lifetimes of the parameters overlap (in practice, the lifetime of the parameter with the shortest lifetime).
|
In that case, can we just say that the lifetime of |
I think the confusion comes from the term
Here lifetime
At the first glance, I thought that |
I think this whole confusion stems from what viewpoint you're taking. From inside the function as a return value, you care about whether a return value lives "at least as long as But from the outside, the opposite is true. You care about whether a value lives "at most as long as
Substitute "longer" for "outside" and we're all on the same page here.
This implies that |
@carols10cents Correct me if I'm wrong, but I think the confusion is that we're discussing two different things:
Honestly, I find that part really hard to get wrong unless you are really trying, e.g. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
&String::from("foo")
}
Or more precisely, what is the valid lifetime of the return value of the function (not inside the function implementation but where the function is called). That's the part I find more interesting and more important to understand. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
"x is the longest"
} else {
"y is the longest"
}
}
fn main() {
let x = String::from("abc");
let s_longest;
{
let y = String::from("defg");
s_longest = longest(&x, &y);
}
println!("{}", s_longest);
} In this example, the lifetime annotations In that chapter of the book, I feel like we should be explaining how lifetime annotations work from the perspective of a function caller, not a function implementer. Or at least we should clearly delineate the two perspectives. I hope this clarify the confusion a bit. |
I just read the book, and I am confused on this part as well. Now i finally understand what ‘at least as long’ means. The part i think isn’t clear is for the input parameter caller have to guarantee that the item that x and y referring live at least as long as some lifetime a, and the callee will guarantee that it return a reference to an item that lives as long as some lifetime a(in other word caller is guaranteed that the ref return will be valid at least as long as lifetime a, so it is safe to use it in that lifetime). So ‘at least as long as’ have different guarantor for input and output. Maybe including this (what caller must guarantee and what is guaranteed by the callee, and the lifetime is the lifetime of the item itself instead of variable that hold the reference) on the book will make it clearer. When i first read this section, i thought this ‘at least as long’ as is all guaranteed by the caller, hence the confusion. And i am not sure why string slices itself that must live at least as long as lifetime a instead of the string that the slice referred. |
Rust noob alert. I do think the book's explanation is correct.
I think, the wording in the book was a bit confusing, but correct nonetheless. |
@carols10cents
What's confusing me is there seems be a gap between the intension:
and what the lifetime annotation provides (according to this explanation):
The phrase some lifetime |
Another explanation confusing me is this definition/description:
It seems to imply an underlying assumption: that the program has already passed the borrow check. However, when we are in the process of borrow checking (a program which maybe correct or not), we don't yet know if the reference is valid at some point - for example, if the subject of the reference might have been moved or dropped. In this context, what does "lifetime of reference" means conceptually? Is it "the span of code/time before its last use" (ignore more complex NLL cases)? This is an example to show what i mean: fn main() {
let r; // ---------+-- 'a --+-- 'c
// | |
{ // | |
let x = 5; // -+-- 'b | |
r = &x; // | | |
} // -+ ------|--------+
// |
println!("r: {}", r); // |
} // ---------+ According to definition/description of "the scope for which that reference is valid", reference |
Hi, finally I had (hopefully) some better understanding of lifetime annotations after reading 1, 2, 3, 4, 5, 6, 7, 8, 9, etc. Preliminaries:
Now let's apply these concepts to This contract can be viewed from two perspectives:
Then the borrow checker can verify it from two perspectives:
|
Just noting, though, that there's a whole new formulation for lifetimes working its way into reality in the Rust compiler, here is a fairly recent link: https://blog.rust-lang.org/inside-rust/2023/10/06/polonius-update.html . |
main
branch to see if this has already been fixedThis overlaps a bit with #1710, but IMO it's a separate issue.
URL to the section(s) of the book with this problem:
https://github.com/rust-lang/book/blob/main/src/ch10-03-lifetime-syntax.md
Description of the problem:
This isn't true as I understand it. For instance, suppose the lifetime
'a
only lasts until immediately after program start. Then (vacuously), both of the function's parameters will live at least as long as that lifetime (they definitely live longer), and the slice returned from the function will also live at least as long as'a
. By the description given, this should be a valid lifetime for the parameter'a
, but it's clearly not - the semantics given might be necessary, but are nowhere near sufficient.Suggested fix:
I'm a Rust newbie, so I have no idea what the correct lifetime is. I tried finding the wording in the spec, to no avail.
The text was updated successfully, but these errors were encountered: