A misconception I had for a while learning about them was that they were labels for “how long this value lives”.
It’s easier, instead, to think of them as constraints for “this value must live for at least as long as this other value”. Lifetimes are usually always related to another value’s lifetime. A reference to self.some_field must be outlived by self, a mutex guard object must be outlived by the mutex itself, etc.
Once you start looking at lifetimes like that, they start to make a bit more sense.
That’s a rather malicious point to make. I did in fact read the article, I’m aware it was the first point, and I just wanted to reword it how I’ve worded it to others. I apologize for not pointing out my exact thought process in making my comment.
I think this is a great post, and lately I’ve been thinking calling these things “lifetimes” was possibly a mistake. This was a choice that Rust made: previous iterations called them “regions” (even worse, in my opinion). It’s not a choice that could be changed now - if there was ever a “breaking documentation change” reframing how lifetimes are described would be one of them - but in future iterations I would reconsider this terminology, even if the typing formalism is the same.
The “lifetimes” that Rust talks about aren’t the “lifetimes of objects” at all - if anything, they’re the lifetime of the reference they parameterize, which definitionally must be less than the lifetime of the referent. But this is muddied more by the subtyping relationship between lifetimes, so that any lifetime which outlives a lifetime can be substituted for it when instantiating a function. This is all rather different from the mental model the term “lifetime” seems to suggest for most people - even in this post, some of the notes seem to come from a slightly wrong mental model, like the complaint that they’re mixed up with generics when that’s just an accurate representation (functions are parameterized by lifetime variables which are susbstituted for concrete lifetimes when the function is instantiated at a call site, the same as any generic parameter).
I think an interesting jumping off point would be to start from the conceptual framework of polonius. This is intended to be an internal change to the compiler to enable more advanced non-lexical lifetimes, but it involves a complete reframing of what a lifetime is to no longer mean a period of code execution but instead a set of borrowed places. Niko Matsakis called this a “loan,” constituting a “set of origins” in his Rust Belt Rust talk in 2019 (link); if you just had “loan” or “borrow” variables, and talked more explicitly about their relationship with “places” (what Christopher Strachey called “lvalues”), you could replace this “lifetimes and borrowing” with just borrowing, using a unified metaphorical framing.
When I was trying to understand this (and lord knows, I still am), I started a thread on the Rust forum that went on way too long but was very informative. Now I see how some of the things I read (including things in the “official” Rust docs) misled me. For example, the oft-repeated but not really correct statement that “MyStruct<'a> means the struct has the lifetime 'a”. After that experience I wrote the thing I wish I had read, and I’m curious what you think of it. (BTW, I was surprised to find the nomicon doesn’t define the term “lifetime”!)
A “lifetime” is a region of code where a reference is assumed to be valid by the compiler. (A “region” is something I don’t have a concise definition for; it’s mostly temporal but a bit lexical.)
MyStruct<'a> is a generic type constructor with one parameter, a lifetime parameter called ’a.
Each time this type is used, the compiler has to choose an actual (concrete) lifetime for ’a.
(N.B. This is the same thing that happens with type parameters. For every use of MyStruct<'a, T> the compiler has to pick a concrete lifetime 'a and a concrete type T.)
The concrete lifetime will be chosen to be consistent with various rules about value and reference lifetimes: ownership rules, NLL rules, etc., some of which are not documented anywhere.
This lifetime may be constrained by other lifetimes through the position of the parameter (not its name — just like type parameters). E.g., fn foo<'c, 'd>(x: &'c MyStruct<'d>, y: &'d str) constrains the value of MyStruct’s first lifetime parameter to intersect y’s lifetime, because the compiler will have to infer a single concrete lifetime that can be used in both places.
If a type has multiple lifetime parameters, e.g. AnotherStruct<'a, 'b>, the type is only valid if 'a and 'b can be inferred to be concrete lifetimes with a non-empty intersection. This is a part of the type system directly — it is true regardless of how the parameters are used inside AnotherStruct, or even if they aren’t concretely used at all (e.g., PhantomData or a trait bound).
The inferred lifetime and mutability of a reference affect its variance. &'a X is usable as a &'b X parameter if 'a and 'b have an intersection. &mut 'a X is usable as a &mut 'b X parameter only if 'a and 'b are equal.
If the compiler can’t infer a non-empty lifetime for all parameters given these constraints, the code is in error. The compiler will try to turn that failure into a human-readable explanation, but that’s a heuristic that doesn’t always help.
Your comments about “intersections” seem a bit indirect to me. If two lifetimes exist at the same moment (say, when a function is called) they necessarily intersect because they both exist at that moment; you can’t instantiate something with a lifetime that does not exist for the period of time the instantiated thing exists (even if it isn’t witnessed by any real reference that exists outside of the system, as you note). This isn’t an arbitrary requirement, it just falls out of this basic principle.
The inferred lifetime and mutability of a reference affect its variance. &’a X is usable as a &’b X parameter if ’a and ’b have an intersection. &mut ’a X is usable as a &mut ’b X parameter only if ’a and ’b are equal.
You’re confused about the variance rules here: &'a mut T is covariant over 'a and invariant over T, whereas &'a T is covariant over both 'a and T. Variance of a type is just variance of any lifetimes in that type. The reason it’s invariant over T is because you can write a T into the reference, so substituting it for a longer-lived subtype of T would result in dangling references.
Yes, the “intersection” thing is muddled. Not to mention, with no return value the lifetimes don’t much matter anyway, which should really be mentioned. Sadly, getting this one right is the key to actually understanding anything! How about:
In function declarations, lifetime parameters constrain the uses of the return value based on the lifetimes of the arguments. But if a lifetime parameter is used in multiple places this can also end up constraining the lifetimes of the arguments. E.g., for fn foo<'c, 'd>(x: &'c MyStruct<'d>, y: &'d str) -> &'d str the compiler will have to infer concrete lifetimes to use for 'c and 'd. Because 'd is used in x, y, and the return value, the compiler has to infer some lifetime for 'd where x and y are both valid, that also includes all the uses of the returned reference. For that to be possible, x and y both have to outlive the last use of the reference. (Though even then the imperfect compiler may not be able to make the inference.)
Ignore the variance point (or better, just plug in what you said)…the example was nonsense.
with no return value the lifetimes don’t much matter anyway
I think you mean this in terms of the fact you don’t generally need to use explicit lifetime parameters on a function? Because they can almost always be elided except in this case. That’s true.
Lifetimes (as in, the concrete ones the compiler comes up with) matter to check dangling references and also to prevent aliased assignment. Lifetime variables exist to trace those variables through otherwise opaque functions. They’re only necessary really because you can return and store references. Other languages can get away with enforcing similar requirements without lifetimes by having references only permitted as a parameter passing mode and not first class types.
That was my compressed version of your second paragraph. I meant “with no return value the lifetime variables don’t much matter” (elided or not). Or as you put it, they’re only necessary because you can return references, so if you aren’t returning anything, it’s not an interesting example.
I’m beginning to think this is like monads, in that once I finally really understand it, I’ll become one of the people desperately trying to explain it (and failing).
I am a proper Rust n00b ramping up as we speak. Even before getting into conceptual challenges of understanding lifetimes, I think it’s important to mention that the syntax was truly jarring for me. In literally every other language I’ve learned, a single unclosed quote means that I have written my program incorrectly in a very fundamental way. I’ve been programming for over 10 years so there’s a lot of muscle memory to undo here. Sorry if this bikeshed-y topic has been discussed to death, but since the article explicitly covers why lifetimes are hard to learn and doesn’t mention this point, I figured it’s fair game to mention again.
I personally like the notation, but I could see how it looks jarring. FWIW, Rust probably borrowed the notation from ML, where single quotes are used to denote type variables. For example, the signature of map in Standard ML is:
val map : (’a -> ’b) -> ’a list -> ’b list.
I got nerd-sniped thinking about how old the use of ' as a sigil might be. It’s used in lisp; I think it’s oldest use might go back to MACLISP in 1966. I think older dialects of lisps required that you say (quote foo) instead of 'foo. See section 1.3 “Notational Conventions” on page 3 of the MACLISP manual (large PDF warning).
Is there a reason that it’s only for lifetimes and not all type parameters? My guess would be because it makes them easy to distinguish (and since type parameters are more common, they get the shorter case), but I could be wrong of course.
I recently saw it in reading the various Cyclone papers, where they introduced it for identifying differing regions, with those having different lifetimes. However, I believe Cyclone was itself drawing inspiration from ML (or Caml).
It also has mention of “borrowing” in what may be a similar fashion.
The ' is an odd one. I had another skim of the history of Standard ML but it isn’t very interested in questions of notation. However, it reminded me that ML type variables are often rendered in print as α, β instead of 'a, 'b, which made me think this might be a kind of stropping. “Stropping” comes from apostrophe, and prefix ' was one form it could take. (Stropping varied a lot depending on the print and punch equipment available at each site.) But this is a wild guess.
I got nerd-sniped thinking about how old the use of ’ as a sigil might be. It’s used in lisp
Oh, yeah, it took me a good six months to stop typing a ( right after the '. The joke about Greenspun’s tenth rule practically writes itself at this point :-).
I’m only mentioning this for the laughs. As far as I’m concerned, any syntax that’s sufficiently different from that of a Turing tarpit is fine. I mean, yeah, watching the compiler try to figure out what the hell I had in mind there was hilarious, but hardly the reason why I found lifetimes so weird to work with.
I haven’t heard this particular challenge before. I came to Rust long after I learned Lisp and Standard ML, so it never occurred to me that it would be jarring, but if you’ve only worked in recent members of the ALGOL family I can see that being the case.
What do you mean muscle memory? Do you usually double your quotes manually? Does your IDE not do it for you?
Not trying to “attack” you or anything, genuinely curious as these kind of syntactical changes are more-or-less invisible to me when writing code due to a good IDE “typing” what I intend based on context for the most part.
As for a single single quote being jarring, I believe it does have some history in LISPs for “unquote” or for symbols. Possibly, the latter case was the inspiration in case of Rust?
Edit: I see there is a much better discussion in the sibling thread regarding its origin.
Ah yes, now that you mention it, “muscle memory” is not the right phrase here. I didn’t mean muscle memory in the (proper) sense of a command that you’ve been using for years, and then now you need to use slightly differently. What I meant was that for years, whenever I saw a single quote, I expected another quote to close it somewhere later in the code. And now I have to undo that expectation.
The point about shared syntax is a big deal. That combined with the difficult to search for “tick” notation, makes lifetimes hard to find online. One other note on syntax is that the name of the lifetime doesn’t really matter at all. ’a is common but sometimes they can be more informative names, which is helpful, but is surely confusing to see in the wild.
A misconception I had for a while learning about them was that they were labels for “how long this value lives”.
It’s easier, instead, to think of them as constraints for “this value must live for at least as long as this other value”. Lifetimes are usually always related to another value’s lifetime. A reference to
self.some_fieldmust be outlived byself, a mutex guard object must be outlived by the mutex itself, etc.Once you start looking at lifetimes like that, they start to make a bit more sense.
I agree!
I’d like to point out, since we are commenting a nice article, that this the first point in the article.
We’re not here to read the articles, we’re here to demonstrate how much smarter we are than the authors.
That’s a rather malicious point to make. I did in fact read the article, I’m aware it was the first point, and I just wanted to reword it how I’ve worded it to others. I apologize for not pointing out my exact thought process in making my comment.
I think this is a great post, and lately I’ve been thinking calling these things “lifetimes” was possibly a mistake. This was a choice that Rust made: previous iterations called them “regions” (even worse, in my opinion). It’s not a choice that could be changed now - if there was ever a “breaking documentation change” reframing how lifetimes are described would be one of them - but in future iterations I would reconsider this terminology, even if the typing formalism is the same.
The “lifetimes” that Rust talks about aren’t the “lifetimes of objects” at all - if anything, they’re the lifetime of the reference they parameterize, which definitionally must be less than the lifetime of the referent. But this is muddied more by the subtyping relationship between lifetimes, so that any lifetime which outlives a lifetime can be substituted for it when instantiating a function. This is all rather different from the mental model the term “lifetime” seems to suggest for most people - even in this post, some of the notes seem to come from a slightly wrong mental model, like the complaint that they’re mixed up with generics when that’s just an accurate representation (functions are parameterized by lifetime variables which are susbstituted for concrete lifetimes when the function is instantiated at a call site, the same as any generic parameter).
I think an interesting jumping off point would be to start from the conceptual framework of polonius. This is intended to be an internal change to the compiler to enable more advanced non-lexical lifetimes, but it involves a complete reframing of what a lifetime is to no longer mean a period of code execution but instead a set of borrowed places. Niko Matsakis called this a “loan,” constituting a “set of origins” in his Rust Belt Rust talk in 2019 (link); if you just had “loan” or “borrow” variables, and talked more explicitly about their relationship with “places” (what Christopher Strachey called “lvalues”), you could replace this “lifetimes and borrowing” with just borrowing, using a unified metaphorical framing.
When I was trying to understand this (and lord knows, I still am), I started a thread on the Rust forum that went on way too long but was very informative. Now I see how some of the things I read (including things in the “official” Rust docs) misled me. For example, the oft-repeated but not really correct statement that “
MyStruct<'a>means the struct has the lifetime'a”. After that experience I wrote the thing I wish I had read, and I’m curious what you think of it. (BTW, I was surprised to find the nomicon doesn’t define the term “lifetime”!)MyStruct<'a>is a generic type constructor with one parameter, a lifetime parameter called ’a.MyStruct<'a, T>the compiler has to pick a concrete lifetime'aand a concrete typeT.)fn foo<'c, 'd>(x: &'c MyStruct<'d>, y: &'d str)constrains the value ofMyStruct’s first lifetime parameter to intersecty’s lifetime, because the compiler will have to infer a single concrete lifetime that can be used in both places.AnotherStruct<'a, 'b>, the type is only valid if'aand'bcan be inferred to be concrete lifetimes with a non-empty intersection. This is a part of the type system directly — it is true regardless of how the parameters are used insideAnotherStruct, or even if they aren’t concretely used at all (e.g.,PhantomDataor a trait bound).&'a Xis usable as a&'b Xparameter if'aand'bhave an intersection.&mut 'a Xis usable as a&mut 'b Xparameter only if'aand'bare equal.Your comments about “intersections” seem a bit indirect to me. If two lifetimes exist at the same moment (say, when a function is called) they necessarily intersect because they both exist at that moment; you can’t instantiate something with a lifetime that does not exist for the period of time the instantiated thing exists (even if it isn’t witnessed by any real reference that exists outside of the system, as you note). This isn’t an arbitrary requirement, it just falls out of this basic principle.
You’re confused about the variance rules here:
&'a mut Tis covariant over'aand invariant over T, whereas&'a Tis covariant over both'aandT. Variance of a type is just variance of any lifetimes in that type. The reason it’s invariant overTis because you can write aTinto the reference, so substituting it for a longer-lived subtype ofTwould result in dangling references.Yes, the “intersection” thing is muddled. Not to mention, with no return value the lifetimes don’t much matter anyway, which should really be mentioned. Sadly, getting this one right is the key to actually understanding anything! How about:
fn foo<'c, 'd>(x: &'c MyStruct<'d>, y: &'d str) -> &'d strthe compiler will have to infer concrete lifetimes to use for'cand'd. Because'dis used inx,y, and the return value, the compiler has to infer some lifetime for'dwherexandyare both valid, that also includes all the uses of the returned reference. For that to be possible,xandyboth have to outlive the last use of the reference. (Though even then the imperfect compiler may not be able to make the inference.)Ignore the variance point (or better, just plug in what you said)…the example was nonsense.
I think you mean this in terms of the fact you don’t generally need to use explicit lifetime parameters on a function? Because they can almost always be elided except in this case. That’s true.
Lifetimes (as in, the concrete ones the compiler comes up with) matter to check dangling references and also to prevent aliased assignment. Lifetime variables exist to trace those variables through otherwise opaque functions. They’re only necessary really because you can return and store references. Other languages can get away with enforcing similar requirements without lifetimes by having references only permitted as a parameter passing mode and not first class types.
That was my compressed version of your second paragraph. I meant “with no return value the lifetime variables don’t much matter” (elided or not). Or as you put it, they’re only necessary because you can return references, so if you aren’t returning anything, it’s not an interesting example.
I’m beginning to think this is like monads, in that once I finally really understand it, I’ll become one of the people desperately trying to explain it (and failing).
You seem to understand it plenty well.
I am a proper Rust n00b ramping up as we speak. Even before getting into conceptual challenges of understanding lifetimes, I think it’s important to mention that the syntax was truly jarring for me. In literally every other language I’ve learned, a single unclosed quote means that I have written my program incorrectly in a very fundamental way. I’ve been programming for over 10 years so there’s a lot of muscle memory to undo here. Sorry if this bikeshed-y topic has been discussed to death, but since the article explicitly covers why lifetimes are hard to learn and doesn’t mention this point, I figured it’s fair game to mention again.
I personally like the notation, but I could see how it looks jarring. FWIW, Rust probably borrowed the notation from ML, where single quotes are used to denote type variables. For example, the signature of
mapin Standard ML is:I got nerd-sniped thinking about how old the use of
'as a sigil might be. It’s used in lisp; I think it’s oldest use might go back to MACLISP in 1966. I think older dialects of lisps required that you say(quote foo)instead of'foo. See section 1.3 “Notational Conventions” on page 3 of the MACLISP manual (large PDF warning).I am the one who proposed the notation (can’t find the reference now, but it’s there, trust me) and yes it is from ML.
Is there a reason that it’s only for lifetimes and not all type parameters? My guess would be because it makes them easy to distinguish (and since type parameters are more common, they get the shorter case), but I could be wrong of course.
Yes because types and lifetimes are different kinds, and yes because types are more common they are unmarked.
Is it?
I recently saw it in reading the various Cyclone papers, where they introduced it for identifying differing regions, with those having different lifetimes. However, I believe Cyclone was itself drawing inspiration from ML (or Caml).
It also has mention of “borrowing” in what may be a similar fashion.
http://www.cs.umd.edu/~mwh/papers/ismm.pdf
Also the syntax there seems to evolve over various papers.
The
'is an odd one. I had another skim of the history of Standard ML but it isn’t very interested in questions of notation. However, it reminded me that ML type variables are often rendered in print as α, β instead of'a,'b, which made me think this might be a kind of stropping. “Stropping” comes from apostrophe, and prefix'was one form it could take. (Stropping varied a lot depending on the print and punch equipment available at each site.) But this is a wild guess.Oh, yeah, it took me a good six months to stop typing a
(right after the'. The joke about Greenspun’s tenth rule practically writes itself at this point :-).I’m only mentioning this for the laughs. As far as I’m concerned, any syntax that’s sufficiently different from that of a Turing tarpit is fine. I mean, yeah, watching the compiler try to figure out what the hell I had in mind there was hilarious, but hardly the reason why I found lifetimes so weird to work with.
Is there also any precedent for this kind of notation in math?
ML is on my list of ur-languages to study: https://news.ycombinator.com/item?id=35816454
I can’t think of instances of
'xin math, but ofcx'is used frequently for the derivative, as well as any kind of transformed version ofx.Or if you just want another variable with a name “like”
x. This is also how it’s used in Haskell.I haven’t heard this particular challenge before. I came to Rust long after I learned Lisp and Standard ML, so it never occurred to me that it would be jarring, but if you’ve only worked in recent members of the ALGOL family I can see that being the case.
What do you mean muscle memory? Do you usually double your quotes manually? Does your IDE not do it for you?
Not trying to “attack” you or anything, genuinely curious as these kind of syntactical changes are more-or-less invisible to me when writing code due to a good IDE “typing” what I intend based on context for the most part.
As for a single single quote being jarring, I believe it does have some history in LISPs for “unquote” or for symbols. Possibly, the latter case was the inspiration in case of Rust?
Edit: I see there is a much better discussion in the sibling thread regarding its origin.
Ah yes, now that you mention it, “muscle memory” is not the right phrase here. I didn’t mean muscle memory in the (proper) sense of a command that you’ve been using for years, and then now you need to use slightly differently. What I meant was that for years, whenever I saw a single quote, I expected another quote to close it somewhere later in the code. And now I have to undo that expectation.
The point about shared syntax is a big deal. That combined with the difficult to search for “tick” notation, makes lifetimes hard to find online. One other note on syntax is that the name of the lifetime doesn’t really matter at all. ’a is common but sometimes they can be more informative names, which is helpful, but is surely confusing to see in the wild.