Skip to content
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

RFC: rename lifetime to lifespan #487

Closed
wants to merge 2 commits into from
Closed

RFC: rename lifetime to lifespan #487

wants to merge 2 commits into from

Conversation

arthurtw
Copy link

No description provided.

@Gankra
Copy link
Contributor

Gankra commented Nov 27, 2014

rendered

@kemurphy
Copy link

-1, seems like unnecessary change in terminology. "Lifespan" wouldn't make the concept any easier to grasp.... Words are just words. A newcomer would still be like "what's a lifespan?" and we'd explain it the exact same way and they'd still be confused.

Also the C++ argument is weak IMO. An object's lifetime is always encoded in the source code through usage of new and delete; it's just that the compiler doesn't reason about it statically. The thing with Rust is that it can make the delete implicit since the compiler is capable of tracking the lifetime at compile time.

@arthurtw
Copy link
Author

@tshepang Right. Maybe just remove long?

error: the life of borrowed value does not span enough

@kemurphy The C++ argument does make a difference. It may seem subtle but is important. In C++, the termination of an object can only be determined at runtime, because the delete operation can be behind an if statement. But in Rust, the validity of a reference pointer is static. Given a line of code, the compiler can deterministically decide if a reference pointer is valid here (even with SEME introduced). The distinction is compile-time vs runtime.

@tshepang
Copy link
Member

I don't remember seeing that usage either.

@arthurtw
Copy link
Author

@tshepang OK I'll leave the wording to you and others. :-)

@Thiez
Copy link

Thiez commented Nov 27, 2014

-1, I don't think lifespan is significantly easier to understand than lifetime and I think this change would lead to more confusion, not less, because so many references to 'lifetime' exist. If we were to change this now new people would keep asking what this 'lifetime' thing is because it would still be mentioned on many websites and in old Rust code.

@ghost
Copy link

ghost commented Nov 27, 2014

-1. First Google hit for pointer lifetime is the Rust guide, followed by MSDN and StackOverflow.

First page for pointer lifespan is about the dog breed.

@matthieu-m
Copy link

Lifetime is tied to runtime and object lifetime in many languages.

Yes, and so is it in Rust. When you see &'a, a does not represent the lifetime of the referred object itself. Instead a in &'a represents a minimal lifetime that the referred object's actual lifetime is guaranteed to exceed.

@shadowmint
Copy link

I agree lifetimes are poorly understood, but I don't accept this RFC will change the status quo in any meaningful way.

For closures, the lifetime is the minimum lifetime of any object it borrows.

A 'static closure cannot borrow any local references, because they have lifetime < 'static. In the following example, 'b is at least 'a, so the code is valid, but without 'b: 'a this code would not be valid because 'b could potentially be > 'a, the lifetime on the closure, and there cannot be closed over.

#![feature(unboxed_closures)]

fn main() {
  let mut x = 10;
  let mut y = 10;
  {
    let mut z = foo(&mut x, &mut y);
    z.call_mut(());
  }
  println!("{} {}", x, y);
}

fn foo<'a, 'b: 'a>(mut x: &'a mut int, mut y: &'b mut int) -> Box<FnMut<(), ()> + 'a> {
  return box move |&mut:| {
    *x = 0;
    *y = 0;
  };
}

For traits, a lifetime is the minimum lifetime bound for the inner data of the object held in the trait's data reference.

'static traits mean that the data value held in the trait cannot have borrowed data in it.

Notice how in the following example the instance of bar is moved out of the enclosing scope, but remains valid. If the lifetime of as_foo was 'static, this wouldn't work, as the lifetime of &x is < 'static.

trait Foo {}

struct Bar<'a> {
  x: &'a int
}

impl<'a> Foo for Bar<'a> {}

fn main() {
  let foo:Box<Foo>;
  let x = 0;
  {
    let bar = Bar { x: &x };
    foo = as_foo(box bar);
  }
}

fn as_foo<'a>(foo:Box<Foo + 'a>) -> Box<Foo + 'a> {
  return foo;
}

For references, the lifetime is the duration it can be borrowed for. &'static can be borrowed forever, for &'a, it means the reference cannot live longer than 'a.

For structs the lifetime is an explicit alternative to the naive ellison provided on functions like:

fn val(&self, x:&int) -> &int { ... } <--- Which lifetime is the return value? 

The guide states:

The length of time that the borrower is borrowing the pointer from you is called a lifetime.

...but it seems to me it means subtly different things in different contexts.

I prefer to think of lifetimes as bounds rather than spans.

When you apply a lifetime to closure or trait argument, you are bounding the potential values that can be passed in that position. In the return argument you are bounding the possible return values.

Using the word 'lifespan' instead of 'lifetime' is almost the archetypal case of bike shedding, but meaningfully, I think it actually proposes the wrong thing.

While the term might be overloaded, a lifetime is a concrete element; it's a bound. A lifespan is not a concrete element, it's temporal range... and that's just not what a lifetime is in rust.

@arthurtw
Copy link
Author

@shadowmint I'm not sure if the unboxed closures are working correctly, because I got an error using the existing closure syntax. I believe the following code (playpen) is equivelant to your FnMut sample:

fn main() {
  let mut x = 10i;
  let mut y = 10i;
  {
    let mut z = foo(&mut x, &mut y);
    (*z)(());
  }
  println!("{} {}", x, y);
}

fn foo<'a, 'b: 'a>(mut x: &'a mut int, mut y: &'b mut int) -> Box<|()|: 'a> {
  return box move |()| {
    *x = 0;
    *y = 0;
  };
}

I got the error:

<anon>:13:6: 13:7 error: captured variable `x` does not outlive the enclosing closure
<anon>:13     *x = 0;
               ^
<anon>:11:77: 16:2 note: captured variable is valid for the block at 11:76
<anon>:11 fn foo<'a, 'b: 'a>(mut x: &'a mut int, mut y: &'b mut int) -> Box<|()|: 'a> {
<anon>:12   return box move |()| {
<anon>:13     *x = 0;
<anon>:14     *y = 0;
<anon>:15   };
<anon>:16 }
<anon>:11:77: 16:2 note: closure is valid for the lifetime 'a as defined on the block at 11:76
<anon>:11 fn foo<'a, 'b: 'a>(mut x: &'a mut int, mut y: &'b mut int) -> Box<|()|: 'a> {
<anon>:12   return box move |()| {
<anon>:13     *x = 0;
<anon>:14     *y = 0;
<anon>:15   };
<anon>:16 }
error: aborting due to previous error
playpen: application terminated with error code 101

In your second example, I believe it has nothing to do with reference lifetimes. It's the copy/move behavior for struct and box, and box is a pointer to a heap allocated memory block. When as_foo(box bar) gets called, bar is copied because it's a copyable struct with only plain old data, so bar can be used after the call. If struct Bar contains non-copyable (linear type) data, then bar is moved after the call, and can no longer be used.

Anyway, whether it's the lifetime of references, closures, functions or structs, I believe all the lifetime thing starts with how far (or to what extent) the borrow of a reference is valid, i.e. the reference lifetime in our current terminology. If a function (or a closure etc.) takes a reference, it may contain (“store”) new references with a lifespan smaller than or equal to its input reference's.

Maybe span is not the most accurate term, because if we color the lines of code where the borrow is valid, the colored lines can be disjointed. My point is it's purely static, determined by the code layout rather than the code execution sequence. (One may argue that it's related to the execution sequence, but the real relationship is both the reference lifetimes and the execution sequence are governed by the code layout.) Even the span can be disjointed, I still feel its static nature describes the concept more precisely.

@shadowmint
Copy link

@arthurtw You can see these examples working on play.rust-lang.org. eg. http://is.gd/b0GacL

Regardless of move / box semantics, the lifetime on Box<Foo + 'a> is a lifetime, and if you rename lifetime to lifespan, we will now have to refer to these as a the 'lifespan' of a trait/closure/struct, not just references.

We can't just change lifetime to lifespan just for references and keep referring to them as lifetimes for closures/structs/traits. That would be completely ridiculous and confusing for everyone.

The point I'm making is that lifetimes are more complicated than simply reference borrows.

A summary of what lifetimes actually mean for all types would be a good first step; followed by details of how the new name is clearer in each case.

I'm skeptical that a simple rename achieves anything, but I'm willing to be convinced otherwise but a thoughtful argument if one turns up. If the nothing else, if the RFC results in an update with more details in the guide, it'll still be worthwhile.

@arthurtw
Copy link
Author

@shadowmint Yes I know your code works with #![feature(unboxed_closures)]. I'm just wondering if it should actually produce a captured variable x does not outlive the enclosing closure error, because the closure shouldn't be able to access or capture a variable beyond its lexical scope.

I felt several disjointed things are mingled under the big lifetime umbrella:

  1. The extent where a borrow is valid. This is static, related to lend and borrow. (We call it lifetime, and that's what the Lifetimes Guide talks about.)
  2. The ownership change due to move, and the allocation/destruction of the owned resource. This is static, related to resource ownership and places where move can happen, i.e. assignments and function calls, whether it's happening to box, struct or closure. (I'm not sure if we formally call it lifetime for the span between the owned resource allocation and destruction. Even it is, calling it lifespan still makes sense because it's static.)
  3. Runtime resource management such as Rc, Arc, Cell and RefCell. This is dynamic, relying on runtime check.

To me, the most important distinction is whether it's a static, compile-time thing or not. I believe using lifespan for case #1 and #2 makes more sense because it's static. (Though it would be great if we can separate #1 and #2 with different terms, but it's at least better than keep using lifetime.)

As to #3, it's a runtime thing. I don't know what's the best way to distinguish them. Maybe just avoid using the term lifetime or lifespan at all? Just call it runtime resource allocation/termination etc. It's not a new thing in other programming languages anyway.

@arthurtw
Copy link
Author

arthurtw commented Dec 1, 2014

After more thoughts, I agree with @shadowmint that just renaming lifetime to lifespan does not really work. To have any meaningful change, the use of the term for case #1 (the extent where a borrow is valid) and #2 (the lifetime of the owned/borrowed resource) in my previous comment must be clearly distinguished.

I still feel that the confusion brought by the term lifetime needs to be dealt with, especially when named lifetimes (i.e. &'a) are used. Allow me to quote the last section of my recent blog post here:

When we talk about borrow, there are three different kinds of “lifetime” involved:

  • A: the lifetime of the resource owner (or the owned/borrowed resource)
  • B: the “lifetime” of the whole borrow, i.e. from the first borrow to the last return
  • C: the lifetime of an individual borrower or borrowed pointer

When one says “lifetime”, it can refer to any of the above. If multiple resources and borrowers are involved, things get even more confusing. For example, what does a “named lifetime” refer to in the declaration of a function or struct? Does it mean A, B or C?

In our previous max function:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {
    if x.f > y.f { x } else { y }
}

What does lifetime 'a mean here? It shouldn’t be A, because two resources are involved and they have different lifetimes. It cannot be C, because there are three borrowers: x, y and the function return value, and they all have different lifetimes, too. Does it mean B? Probably. But the whole borrow scope is not a concrete object, how can it have a “lifetime”? Calling it lifetime is just confusing.

Some may say it means the minimal lifetime requirements to the borrowed resources’ lifetimes. That makes sense in some way, but how can we call the minimal lifetime requirements “a lifetime”?

If we keep calling the valid scope of an owned/borrowed resource or a borrower pointer a “lifetime”, we shouldn’t call &'a “lifetime 'a”, because it really isn’t any resource or pointer’s lifetime. In my blog post, I called it borrow scope, which is not any single borrower’s scope but the union of all borrowers’ scopes, or the span from the first borrow to the last return.

Is it worth submitting another RFC? Or this topic is simply too controversial to have any outcome?

@thestinger
Copy link

I don't think either is a better name, so I don't think anything should be changed. There's no need for churn if it's not actually providing a significant improvement. Keep in mind that people who already know Rust are going to be calling them lifetimes years into the future, so newcomers are just going to have an extra term to learn. You can't really kill the momentum on stuff like this. It's hard enough to get people to call [T, ..n] a (fixed-size) array, [T] a slice and only Vec<T> a vector.

@arthurtw
Copy link
Author

arthurtw commented Dec 3, 2014

Closing this PR. I agree the intent of this RFC does not justify the churn.

We should however clarify/formalise how we call 'a. Doing a quick grep in rust-lang/rust repo, there are at least four different usages, with “lifetime parameter” appearing frequently:

lifetime '           20%
lifetime parameter   68%
lifetime specifier   6%
named lifetime       6%

The inconsistency in the rust repo itself shows a unified naming is missing. If naming it completely different (such as borrow scope) is unlikely, at least we can use "lifetime parameter" as an official term to name 'a, e.g. calling 'a "lifetime parameter 'a", which is much better than calling it "lifetime 'a".

We need a consistent naming anyway. I will submit a short RFC for officially naming 'a "lifetime parameter 'a".

@arthurtw arthurtw closed this Dec 3, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants