"lifetime" from terminology perspective

My hypothesis is that Rust uses the term lifetime in a confusing way, which may lead people learning Rust to make wrong assumptions, and form an inaccurate mental model of lifetimes.

In computer science, outside of Rust, the term "lifetime" has a broader meaning of any time span from creation (allocation) of an object to its invalidation (deallocation). The term can be applied to garbage-collected reference types, or malloc and free, or C++ new and delete. Objects can freely move between scopes during their "lifetime" (in Rust borrowing prevents moves). GC languages automatically extend objects' "lifetimes" (Rust can't).

In Rust, it comes as a surprise that only references have a lifetime. Other types can be said to live for a certain amount time, but that is not their lifetime. It's unexpected that lifetimes are limited by the scopes they originated from, and don't fully match how long a borrowed object actually lives for.

Without understanding of finer borrowing details, it seems weirdly inconsistent that a generic T: 'static bound on a function requires a T == &str to live until the very end of the program, regardless of how long it's actually needed for in the function, but a T == String satisfies the 'static lifetime bound even when it doesn't live forever, and could be destroyed in the function or soon after the call.

&'a str is a reference that has a lifetime, but Box<str> is also a kind of a "reference" type, and it has its own "lifetime", but these are different kinds of references with different kinds of lifetimes!

I think it could be helpful to adjust Rust's terminology and migrate the term lifetime to something else, but I'm not sure what other term would be better. Any ideas?

11 Likes

"borrow time"?

1 Like

The term for lifetime of a value including non-reference one is called scope: 2094-nll - The Rust RFC Book

For reference lifetime, there is maybe another term origin, a set of loans: Borrow checking without lifetimes · baby steps

2 Likes

I've read comments where Niko agrees and wishes that he stuck with the term "regions". Some time in the past he thought "regions" would be too easily confused with memory regions if I recall correctly. Instead we get the lifetime confusion.

The region terminology is still used in the compiler itself.

I agree it would be much better if the mainstream terminology changed, but think it's unlikely by now. Though not quite as unlikely than clarifying exclusiveness versus mutability.[1][2]


  1. At least there's no lifetime keyword. ↩︎

  2. Sometimes I wonder if the pitch to change &mut to &excl or whatever would have gained more traction had it not been comboed with getting rid of mut bindings... ↩︎

5 Likes

I think describing the distinction this way is moving away from the model that is accurate and consistent with other terminology.[1] Reference types have a lifetime variable[2], which is an under-approximation of “the lifetime of” (in the standard outside-of-Rust sense) their referent. References describe the lifetimes of their referents.

That is, in this frame, the terminology problem is that references mention one lifetime variable, which is different from the outside-of-Rust meaning of "this value (which happens to be a pointer) has this lifetime".

English “of” is not precise enough for this job because “the X of Y” is naturally used for any unique X associated with Y. So I think what we need is to have distinct phrases to replace both senses of “the lifetime of”. I suppose this can consist of replacing the noun somehow, “the [lifetime-mentioned] of &'a T is 'a”, but I think it will be much more productive to find new good phrases than to try to globally replace the name “lifetime”.


  1. Presuming not substituting another word. ↩︎

  2. Never a concrete lifetime, except for 'static! ↩︎

4 Likes

I would like to put forward the term "lease" for consideration. It just occurred to me having read the post, curious what people think, good or bad!

From wikipedia

A lease is a contractual arrangement calling for the user (referred to as the lessee) to pay the owner (referred to as the lessor) for the use of an asset.

It's a noun. The whole contract notion seems relevant. There is an owner, and a "borrower" (lessee?). It has a negotiable duration, and there are terms for leasing (i.e. &shared or &mut exclusive).

5 Likes

"Loan" is another word used in some Rust technical documentation / blog posts. But there's a distinction between a lifetime/region and a loan: places have loans which are associated with a lifetime, but the loans can expire before the lifetime does (allowing more things to compile).

  • A loan comes from some borrow expression like &foo. A loan L0 is “live” if some live variable contains a region 'a whose value includes L0. When a loan is live, the “terms of the loan” must be respected: for a shared borrow like &foo, that means the path that was borrowed (foo) cannot be mutated. For a mutable borrow, it means that the path that was borrowed cannot be accessed at all.
    • If an access occurs that violates the terms of a loan, that is an error.

Some more examples are in this Polonius update and the linked materials.

Why the name "Polonius"?

The name comes from the famous quote "Neither a borrower nor a lender be", which comes from the character Polonius in Shakespeare's Hamlet.

4 Likes

I really wanted to reduce this to a one-liner, but it is difficult because this isn't valid Rust syntax:

let borrower = &'loan_duration owner;

Syntactically, the lifetime annotation never appears where the borrow takes place, so we need a separate mechanism, like a comment, to make the connection. This one-liner is also only useful as a first approximation, for example to introduce borrowing to beginners. It quickly falls apart when trying to use this for analyzing code with even minor complexity.

The loan term is something I haven't been using in communication or teaching, but it's more-or-less how I internally rationalize borrows. The "lifetime" of a borrow is the duration of the loan. And I haven't come across any cases that invalidate this basic assumption. I do feel that using lifetime as the term for "constructor to destructor" is best, since it aligns with common usage in other domains. "Loan duration" is not half bad when speaking about borrowing in Rust.

1 Like

I think something like scope (scope of a loan) could be a useful name. In the original borrow checker there was almost 1:1 relationship between lifetimes and lexical scopes, and even in NLL, lexical scopes are still the upper limit for a lifetime of a reference (loans can be shortened, but not extended beyond their original scope).

In cases like Trait<'a> or Struct<'a>, it could be clearer that it's not just any lifetime variable 'a describing how long something lives, but it could be described as some outer scope that a reference borrows from.

It could help understand why returning &Obj::new() from functions isn't working. When "lifetime" is understood in the broad sense, then returning &'a Obj can make sense, because 'a could be describing the arbitrary lifetime/lifecycle of Obj. But if it was not a lifetime, but a scope, then it would be clearer that return leaving function scope with &'inside_function Obj is dangling, and the object has been borrowed in a wrong scope, and it even makes sense that assigning to &'outside_of_function mut Obj can work. It may even help understand creation of self-referential structs: returning (Vec<T>, &'vec T) could make sense to people, because the lifetime annotation could be referring to how long the Vec lives (it's still alive when returned), but framing it as returning (Vec<T>, &'scope_inside_function T) makes it clearer it's not about how long Vec lives, but where the loan is allowed to exist.

However, scope has overlap with the lexical scope, so this could add confusion when references don't live exactly from { until }, and calling <'a> a "scope annotation" sounds odd to me. Are there synonyms for scopes?

3 Likes

Scope would probably be an improvement over lifetime. My ability to reason about the borrow checker improved immensely once I understood that references are checked at compile-time and everything the compiler knows about references must be knowable without running the code. A related revelation is that Rust reference lifetimes are types, not values. The common interpretation of "lifetime" applies to individual values at runtime, so applying it to Rust references leads to confusion on multiple dimensions.

If the word scope is already overloaded, maybe qualifying it with "borrow scope" is enough to distinguish it from lexical scope, or perhaps a completely different word like "region" that was mentioned earlier.

4 Likes

Borrow scope sounds good.

I'd call it a "borrow scope annotation", and probably a "borrow annotation" for short, it identifies the scope of the borrow/a borrow/allowed borrows.

E.g. "The borrow annotations are usually elided and filled in for us by the compiler, but sometimes we need to write them down so the borrow checker can disambiguate."

I don't like "scope" because of the conflation with lexical blocks, which already confuses some people.[1] And like @vague pointed out, "scope" is sometimes used to mean value liveness, so you wouldn't necessarily be getting away from giving the two concepts distinct names.

As for synonyms other than region... I looked but nothing jumped out to me. Duration? Allowance? Bailiwick is fun to say.[2]


  1. The confusion is compounded by official documentation using blocks to try to explain lifetimes, which should be fixed. ↩︎

  2. error: lifetime may not live long enough not your bailiwick ↩︎

6 Likes

I’ve occasionally used “stack frame” with some success, but that’s probably too loose/inaccurate for general use without adding lots of context.

Hey I was thinking about an RFC for a ‘comptime lifetime, what do you think about this syntax for compile time Rust?

Also, does anyone with knowledge of Rustc internals know of it actually makes sense to signal compile-time programming with lifetimes like this?

I’m just trying to avoid the heinous trait level programs and use Result to make compiler errors better I reckon (see 2nd, 3rd picture)

Imagine we wanted to check bounds on arrays and array operations exactly once at compile time (to prevent shape bugs making AI)

Please realize I made this up!

/// trait for nd-arrays
pub trait Shaped<const RANK: usize = 0>
{
	/// evaluated methods exactly once
	fn shape(&’comptime self) -> [Ax; RANK];
}

/// structure of Axis
pub struct Shape<‘c: comptime, const RANK: usize = 0>
{
	names: ’c [str; RANK],
	bounds: ’c [usize; RANK]
}

pub struct Nd<
	‘c: comptime, 
	const RANK: usize = 0, 
	T = BigDecimal> {
	entries: KV<Shape<‘c, RANK>, T>
}

pub struct Ax<‘c: comptime>(‘c str, ‘c usize)

impl<‘c: comptime, Lhs, Rhs> Broadcast<Rhs> for Lhs
where
	Self: Shaped,
	Rhs: Shaped,
	// compile time TypeGuard function
	‘c Self: <Self as Merge<Rhs>>::compatible(‘c Rhs)
{
	type Output = Self;

	fn compatible(
		self: ‘c Self, 
		other: ‘c Rhs,
	) -> anyhow::Result<impl Shape> {
		let mut shape = KV::Zero;
		let mut errors = KV::Zero;
		for Ax(name, bound) in self.shape() {
			shape.push((name, bound))
		}
		for Ax(name, bound) in other.shape() {
			if let Some(self_bound) = shape.get(&name) {
				match (self_bound, bound) {
					(1, n) if n > 1 => shape.insert(
											name, n)?,
					(n, 1) => {}, // ok no-op
					(n, n) => {}, // ok no-op
					(n, m) => errors.push(
			anyhow!(“{n}, {m} are incompatible”)
					)
				}
			} else {
				shape.push((name, bound))
			}
		}
		match errors.is_empty() {
			Ok(shape)
		}
	}
}

Main point would be using ‘c: comptime to signal when things ought to happen during the compilation of the code and not at runtime.

Function invocation in the where clause could be exclusive to comptime lifetimes to run a normal function once and report an error right away if it pops up

This code syntax seems inefficient. Could functions be better?

These errors are awful and I could surely write better ones in a normal Result.

That's just const.

1 Like

Please make a separate thread for your proposal, where you could get feedback on it. A proposal for a new kind of const memory management and its syntax is not really related to the discussion about terminology for the existing feature.

5 Likes

I've been thinking about this for a while, now. I fully agree lifetime is quite a bad name.

In fact, when I actually need to talk about the "life time" of a variable/live value, in opposition to a Rust lifetime, I end up having to use life-span for the former to disambiguate.

Region is indeed a nice term, using the space dimension, and similarly, duration, if using the time dimension.

I like duration insofar 'static, which is also a horribly named thing, could be renamed as 'forever.

Finally, T : 'duration, conceptually-speaking, is

  • T : UsableFor<'duration> , or
  • T : UsableWithin<'duration>/T : UsableWithin<'region>.

This helps illustrate the difference between
String : UsableFor<'forever> and a &'forever String, for instance.

And the icing on the cake would be to have 'forever >= 'duration notation[1] as well :smile:


  1. some of you may retort that 'forever <: 'other, since &'forever T <: &'other T and 'x -> &'x T is covariant. But not only is "subtyping for lifetimes" something devoid of meaning (we need to talk of &T types to bring subtyping into the discussion), but also the polarity of 'a <: 'b and the variance of 'x -> &'x T, together, can be arbitrarily picked in whichever direction we want. And, in fact, if one stops and properly thinks about this, 'x -> &'x T being contravariant in 'x with 'forever >= 'other is a way more intuitive mental model ↩︎

5 Likes

The terminology used to be that way.

3 Likes

Personally I call the 'a thing a borrow context. I started to think about it this way when borrow checker finally clicked for me – I realized that 'a could actually refer to multiple lifetimes of multiple different borrows. Since then, I don't think about the 'a as a "lifetime" at all.

This is a similar model as Niko's Borrow checking without lifetimes · baby steps (already linked). I've also seen similar explanation on reddit (can't find a link now though).

Niko's explanation is a bit technical, so perhaps I'll try to explain this borrow context model in my words.

So basically, every reference or lifetime parameter is associated with a set of variables it can borrow from. I'll be using '{list of borrows} syntax for that.

let x: i32 = ...;
let xref: &'{x}i32 = &x;
This explanation is a bit lengthy – click to expand

There could be multiple variables in the borrow context:

let maybe: &'{x, y}i32 = if rand() { &x } else { &y }

Here maybe may borrow from either x or y. But from borrow checker's point of view – it's the same as if it borrowed from both.

Borrows may be mutable:

let mut v = vec!['a']
let a: &'{mut v}mut char = &mut v[0];

That mut mut might be redundant, but... remember that shared references may still hold the exclusive borrow?

let a = Mutex::new(X::new());
let xref: &'{mut a} X = &*a.get_mut();
a.lock(); // nope!
println!({xref});

Here xref has type &X, but it still borrows a mutably!.

So what's an 'a then? An named borrow context.

fn first_word<'a>(s: &'a str) -> &'a str { s.split_whitespace().next().unwrap() }

let hello = String::from("Hello, world!")
hello::<'{hello}>(&hello); // "Hello"
hello::<'static>("hi"); // "hi"

So in these two invocations, you can imagine the 'a parameter of first_word gets assigned to '{hello} in the first invocation and then to 'static in the second one.
So, what is 'static then? It's simply an empty borrow context! 'static = '{}. So, the &'{} str borrows from "nowhere" (or at least from nowhere interesting – for borrow checker borrowing from statics is like not borrowing at all).

Let's try structs:

struct Foo<'a> { x: &'a i32, y: &'a i32 }

let xy: Foo<'{x, y}> = Foo{x: &x, y: &y};
let xx: Foo<'{x}>    = Foo{x: &x, y: &x};
let s:  Foo<'{}>     = Foo{x: &5, y: &6}; // 'static

Note: In the xy case both references have the same borrow context (because they're both 'a) – 'a = '{x, y}), so xy.y would behave as if it borrowed x too!

What about bounds?

  • where T: 'a
    

    This means that T may borrow only from a given borrow context 'a. Eg. T: 'static means T: '{}, which means that T may only borrow from empty set – cannot borrow at all.

  •  where 'b: 'a
    

    This adds a requirement that 'b must be a subset of 'a. Or equivalently, that 'a is forced to be a superset of 'b. Example:

    struct Foo<'a, 'b: 'a> {
        x: &'a i32,
        y: &'b i32,
    }
    
    fn frob(foo: &mut Foo) { foo.x = foo.y }
    
    let mut xy: Foo<'{x, y}, '{y}> = Foo { x: &x, y: &y };
    frob(&mut xy);
    

    Here, the assignment in frob requires that foo.x's borrow scope contains the foo.y's borrow scope (because x can "inherit" all the y's borrows via the assigment). And this compiles, because Foo has the 'b: 'a bound. And from the other side – due to that 'b: 'a bound, the example xy: Foo struct gets its 'a inferred to be '{x, y}.

I hope that's enough examples to convey the idea :slight_smile:

You might say "but the name of variable itself doesn't tell you how long the borrow lasts!". Yup, that's true. Fortunately that's borrow checker's job to check! (Whether borrow context is "valid").

3 Likes

I've been thinking about this for a while, now. I have concluded that the "lifetime" terminology is pretty good. Despite ambiguity in its overloaded usage.

My reasoning is:

We have variables. Variables come into existence (occupying a space in memory and initialised) and are usable until they go out of existence (That memory then being free for use for something else). Variables have a "lifetime".

We have references. References are themselves variables. References also have a "lifetime", determined by their creation and destruction.

However, there is a third "lifetime" in play. A reference "points to" a variable, which itself has a lifetime. It is often that we are concerned with the lifetime of the thing a reference points to not the lifetime of the actual reference itself. In fact we are not so worried about even about the actual lifetime of any particular variable a reference points to, after all the same reference can be changed to point to many different variables, all with different lifetimes.

In summary we have three possible use of "lifetime":
The lifetime of variables.
The lifetime of references. which are variables themselves.
The lifetime of whatever a reference points to (at the time).

Now, we could try to disambiguate that by introducing different terms for them, "scope", "region", "duration" whatever. My feeling is that just muddies the waters further.

3 Likes