Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does a scoped lifetime in rust actually mean?

Tags:

rust

lifetime

So, in:

fn v1<'a> (a:~[&'a str]) -> ~[&'a str] {
  return a;
}

#[test]
fn test_can_create_struct() {
  let x = v1("Hello World".split(' ').collect());
}

I know, I've read http://static.rust-lang.org/doc/master/guide-lifetimes.html#named-lifetimes, but I don't understand what this code actually does.

The function is basically parametrized like a generic fn but with a lifetime, is what I've seen said on the IRC channel, but lets imagine that is the case, and we have a L, which is some specific lifetime structure.

Apparently I'm implicitly calling:

v1::<L>("Hello World".split(' ').collect());

..but I'm not. The lifetime being passed to this function is an instance of a lifetime, its not a TYPE of lifetime, so that comment doesn't make any sense to me.

I mean, I basically understand whats going on (I think): The returned ~[&str] has the same lifetime as the scope of the caller, presumably the test_can_create_struct() function. That's because (as I understand it) the function v1 is invoked with the lifetime instance from the calling function.

Very confusing.

Then we have some other examples like: https://gist.github.com/bvssvni/8970459

Here's a fragment:

impl<'a> Data<'a> {
  pub fn new() -> Data<'a> {
    Data { a: None, b: None }
  }

  pub fn a(&'a mut self, a: int) -> State<'a, Step1> {
    self.a = Some(a);
    State { data: self }
  }
}

Now here I naively assumed that the Data<'a> means that the lifetime instance for the function a() is the same.

i.e. If you create a Data (let blah = Data::new()) and call blah.a(), the lifetime is inherited from the create call; i.e. the State object returned will exist for as long as the parent Data object does.

...but apparently that's wrong too. So I have just no idea what the lifetime variables mean at all now.

Help!

like image 282
Doug Avatar asked Mar 05 '14 03:03

Doug


1 Answers

So the easiest way to answer this will be to take a step back and walk you through what a lifetime actually is.

Lets take a simple function:

fn simple_function() {
  let a = MyFoo::new();
  println("{}", a);
}

In this function, we have a variable, a. This variable, like all variables, lives for a certain amount of time. In this case, it lives to the end of the function. When the function ends, a dies. The lifetime of a, can then be described as starting at the beginning of the function, end ending at the end of the function.

This next function won't compile:

fn broken_function() -> &MyFoo {
  let a = MyFoo::new();
  return &a;
}

When you do &a, you are borrowing a reference to a. The thing about borrowing, though, is that you are expected to give the thing you borrowed back. Rust is very strict about this, and won't let you have references you can't return. If the thing you borrowed your reference from isn't around any more, you can't return the reference and that's just not on.

What it means for our broken_function is that, because a dies at the end of the function, the reference can't escape the function, because that would make it outlast a.

The next step is this:

fn call_fn() {
  let a = MyFoo:new();
  {
    let a_ref = &a;
    let b = lifetimed(a_ref);

    println!("{}", *b);
  }
}

fn lifetimed<'a>(foo: &'a MyFoo) -> &'a MyBar {
   return foo.as_bar();
}

Here are two functions, call_fn and lifetimed, there's some subtle stuff going on here, so I'll break it down.

In call_fn I first create a new instance of MyFoo and assign it to a, then, I borrow a reference to a and assign it to a_ref. The thing with borrowing is that when you do a borrow, the lifetime information is transfered from the variable you are borrowing, into the reference itself. So now a_ref, as a variable, has its own lifetime, which starts and ends at the beginning and end of that inner scope, but the type of a_ref also has a lifetime, the one transferred over from a.

Concrete lifetimes can't be named, but lets pretend we can do it anyway by using numbers. If the lifetime of a is #1, then the type of a_ref is &'#1 MyFoo. When we pass a_ref to lifetimed, the compiler fills in the lifetime parameter 'a like it does for other type parameters. lifetimed's return type is a reference with the same lifetime, so the compiler fills in the space there. Effectively making a unique call to lifetimed(foo: &'#1 MyFoo) -> &'#1 MyBar.

This is why lifetimes appear in the type parameter list, they are part of the type system, and if the types don't match up, that's an error. The compiler works out the lifetimes needed in order for the function to compile, so you never have to worry about it, but won't look outside the current function to get more information. You need to use the parameters to tell the compiler about the function you're calling so it knows that everything is ok.


NB: There is one lifetime you can explicitly name. 'static which is the lifetime of things that last for the entire length of the program.

like image 172
Aatch Avatar answered Sep 28 '22 06:09

Aatch