Lifetime and generic in plain English

Hello,

I am learning Rust, and I try to express in plain English several declarations.

In order to check whether I understand well or not, could you please tell me if my wording is correct or not ?

Note: do not hesitate to be picky.

For every lifetime "a", define a type "MyString<'a>" that contains a reference to a "str" whose lifetime is "a".

struct MyString<'a> {
    name: String,
    value: &'a str
}

For every lifetime "a", and for every type "T", define a type "MyType<'a, T>" that contains a reference to "T" whose lifetime is "a".

struct MyType<'a, T> {
    name: String,
    value: &'a T
}

For every lifetime "a" and every lifetime "b", and for every type "T", define a type "MyOtherType<'a, 'b, T>" that contains a reference to "T" whose lifetime is "a", and a reference to "T" whose lifetime is "b".

struct MyOtherType<'a, 'b, T> {
    name: String,
    value: &'a T,
    other_value: &'b T
}

For every lifetime "a", define a function "longest_by_str<'a>" that takes 2 references to "str" whose lifetimes are "a" as parameters, and returns an optional reference to the type "MyString" whose lifetime is "a".

fn longest_by_str<'a>(string1: &'a str, string2: &'a str) -> Option<MyString<'a>> {
    if string1.len() > string2.len() {
        return Some(MyString { name: String::from("string1"), value: string1 });
    }
    if string2.len() > string1.len() {
        return Some(MyString { name: String::from("string2"), value: string2 });
    }
    return None;
}

Thanks

1 Like

I find this phrasing to be ambiguous. I prefer to say that the reference is annotated with the lifetime, rather than saying it is the lifetime of the reference (or worse, of the str).

It's not really the lifetime of the reference. The reference can exist for a shorter time than 'a. (but not longer!)

It's also not the lifetime of the str. The str can exist for a longer time than 'a. (but not shorter!)

5 Likes

Hello Alice,

Thanks.

I realise that I was totally off the mark... that's scary :scream:

If I understand, correct me if I am wrong, it is better to focus on (referenced) values than on references (to values). A lifetime defines the minimum life span for a (referenced) value.

I prefer to just not use the same language for "liveness scope of a value (or variable)" and "lifetime" (the things that look like 'a). But making sure you're not conflating the two is the more important point.

The next step is probably to add trait and lifetime bounds into the wording.

1 Like

Hello Quibenot,

Thank you for your remark that raises an interesting point.

Please note that I am pretty good in English. However, my mother tongue is French. Thus linguistic nuances may escape me.

If I understand correctly:

  • The "liveness" of a value is the scope of the variable associated with the value. I prefer to call it "scope," as this word is widely used.
  • The "lifetime" of a value is the minimum (required) life span for a (referenced) value.

And, thus, the "liveness" (or "scope") of a value (associated with a variable) is greater or equal than its "lifetime."

I need to digest this statement, and find examples to illustrate the concepts involved.

Regards,

Denis

I would say your interpretations are plenty good enough for a casual conversation[1].

The problem I've always found with English (or any spoken language) is that it tends to be ambiguous, so you tend to rely on a lot of shortcuts or examples. Then you hope that the person on the other side knows enough about the topic to fill in the gaps well enough that it doesn't matter.

For some more examples to practice your understanding, how would you describe the following in English?

fn longest_by_str<'a, 'b: 'a, 'c: 'a)(string_1: &'b str, string_2: &'c str) -> &'a str { ... }

struct LifetimeAndT<'a, T: 'a> {
  value: &'a T,
}

struct LifetimeAndTMut<'a, T: 'a> {
  value: &'a mut T, 
}

impl<'a, T> LifetimeAndTMut<'a, T>
where
  T: Iterator,
  T::Item: 'a,
{
  type Item = T::Item;
  fn next(&mut self) -> Option<Self::Item> { ... }
}

  1. Tangent: how do you like to pronounce &str? I've always found those sorts of words clunky when spoken out loud. Similarly, the plural form of x, xs, sounds perfectly fine when spoken aloud, but I've always thought it looks odd in source code. ↩︎

2 Likes

I think you understand pretty well. I would perhaps say

The "liveness" (or "scope") of a value is greater or equal than the "lifetime" of any borrows of the value.

The distinction becomes more important once you start thinking about nested borrows -- borrows of types which themselves have lifetimes. Or about the liveness scope of values whose types have lifetimes.


The lifetime of a borrow cannot outlive the borrowed value's scope.

    // Does not compile
    {
        let local = String::new();
        let borrow: &'static String = &local;
    } // local drops here

But the lifetime of a borrow can be greater than the scope of the borrow itself.

// This one compiles
fn example<'a: 'a>(string: &'a String) -> &'a String {
    {
        let local_1: &'static str = "";
        let local_2: Box<&'static str> = Box::new("");
        let local_3: &'a str = string;
    } // locals drop here

    string
}

So lifetimes in a value's type are not limited by the liveness scope of the value.

1 Like

Hello Quinedot,

Thank your for you response.

I am sorry to be pernickety on the vocabulary, but I just want to make sure that I understand. This is because English is not my mother tongue.

When you say "a borrow," you mean "a borrowing," I guess.

You "borrow" a value by passing the reference of this value. Thus, "to borrow" is the action that consists of passing the reference of a value.

To borrow (verb): to get or receive something from someone with the intention of giving it back after a period of time.

// "borrower" borrows "borrowed_value".
let borrowed_value: String = String::from("a");
let borrower: &String = &borrowed_value;  // That is a "borrowing"

Thus, I guess, we should talk of the lifetime of a "borrowing".

But perhaps that I am off the tracks...

Regards,

Denis

If you'd like a formal term, you can say that MyString is a Type constructor - Wikipedia.

In a way, it's a "type function" to which you provide the lifetimes (or types or ...) as arguments, and it "returns" you the specific type.

I mean "something with a lifetime" but you can just think of it as "a reference", as it's almost always ultimately a & or a &mut. "Borrow" is just a more generic term for when you have a type that contains or takes the place of the reference.

Taking a reference to v creates a borrow of v; the &v is a borrow of v:

let v = vec![()];
let rv = &v;
// rv is a borrow of v
// rv has type `&'x Vec<()>` for some inferred lifetime `'x`
1 Like

Hello Scottmcm,

Thank you for your response.

Indeed, I think that the term "type function" (or maybe "constructor") is a better appellation. This term emphasizes the fact that it is a function that returns a variable of a given type.

The term "constructor" may not be a good choice, since it is too much connoted "object-oriented development." However, in reality, that's what a "type function" does : it "constructs" an instance of a given type.

Regards,

Denis

Hello Quinedot,

Ah, OK, I see : "something with a lifetime."

Things are starting to come together.

I think that when I have a good understanding of the important concepts that you don't encounter with other popular programming languages, I'll start writing documents in French (for French students - that don't have time to dig into the details).

Thanks a lot for your remarks !

Denis

No.

It is a linguistic fact of English that verbs can be nouned and nouns can be verbed (see what I did there)? Thus, talking about "a borrow" is correct. It's a noun in that context.

On the contrary, "a borrowing" sounds very much not English, not natural. It feels like it's being forced.

2 Likes

Hello Michael-F-Bryan,

Thank you for your response.

I will look into the exercises you suggest. But, not right now. I need to consolidate the basics first.

I mean, it’s easy to start programming with Rust right now and go ahead by a series of trials and errors. You progress and you can write your software. But by doing so, you don't really understand what you manipulate. You just adapt the code based on the compiler output. But I think, especially with Rust, it is important to "pause and dig" in order. I develop for 25 years now (C, Java, Perl, Python, Go, TCL, JS...). And, as I learn Rust, I discover new concepts.

Reagards,

Denis

1 Like

Hello H2CO3,

Yes, you're right : "material dug from a borrow pit to provide fill at another".

Thus we can say:

    fn the_borrower(param: &u8) { println!("param = {}", param) }
    let value: u8 = 1;
    // "&value" is **THE** borrow of the variable "value."
    // "a_borrow" is **A** borrow of the variable "value."
    let a_borrow: &u8 = &value; // The borrow occurs here.

    // The function "the_borrower" borrows the value of the variable "value."
    the_borrower(&value); // The borrow occurs here.

The term "borrow" may designate one of these things :

  • (a) the action (to borrow something).
  • (b) the reference to a variable (&value).
  • (c) a variable that contains the reference to a variable (let the_borrow = &value). You may have more than one borrow to a variable.

The lifetime of a borrow (b) can be greater than the scope of the borrow (c) itself.

For someone who is not a native English speaker, this can be confusing :wink:

Would it be possible to define in the official Rust documentation 3 separate terms for the 3 things mentioned above? I'm sure this would make it easier for many people to adopt Rust.

Regards,

Denis

I could add my understanding..
(attn.: be carefully, I am beginner in rust lang and beginner in spoken en. lang.)

References live in scope. When a scope closes here } all its references die.
But, in situations if developer needs some reference after it's scope is gone, he asks compiler "Pls, hold that ref a little longer..". And compilers broke the rule.
How long? Until the second ref is alive.

So, when you see lifetime annotation, that means some reference has unusual age.

Not necessarily. Lifetime annotations just attach a name to how long an object is alive for so we can talk about it in types and function signatures.

They don't actually tell the compiler to make something live longer than it normally would - it just attaches a name to something that was already there so the borrow checker can go through making sure all the names you wrote are consistent with how the objects are used.

5 Likes

I have digested all your interventions, and that's thesum-up.

Point 1: lifetime vs scope

  • "... lifetimes ensure that references are valid as long as we need them to be."
  • "... lifetime, which is the scope for which that reference is valid."

Source: Validating References with Lifetimes

Thus, the term "lifetime" refers to references, while the term "scope" refers to variables.

From my point of view, the reference the refers to a (given) value and the referenced value are indissociable. They are like the two sides of the same coin.

  • The lifetime of a reference or a (run-time) value.
  • The scope of a variable.

I find this article very interesting. It talks about scope and lifetime in the context of other languages than Rust (for example: C). We definitely use the term "scope" for an "identifier" (that is, the variable) and the term "lifetime" for the "identified run-time object" (that is, the value).

Point 2: variable vs value

A variable temporarily owns a value <=> a value is temporarily owned by a variable.

Indeed, the ownership over a value may be moved from a variable to another variable.

    // `a` is a pointer to a _heap_ allocated integer
    let a = Box::new(5i32);

    println!("a contains: {}", a);

    // *Move* `a` into `b`
    let b = a;
    // The pointer address of `a` is copied (not the data) into `b`.
    // Both are now pointers to the same heap allocated data, but `b` now owns it.

Source: https://doc.rust-lang.org/rust-by-example/scope/move.html

Point 3: references, variables and values

The &s1 syntax lets us create a reference that refers to the value of s1.

source: References and Borrowing.

  • References refer to values <=> values are referred to by references.
  • Variables own values <=> values are owned by variables.

Please not that the value owned by a variable can be a reference.

Thus, I'd say:

The &s1 syntax lets us create a reference that refers to the value (temporarily) owned by (the variable) s1.

Questions:

  • Can we have a value that is not owned by a variable ? I'd say no. Otherwise, how could this value be accessed ?
  • Is a value always referred to by a reference ? If a value is always owned by a variable, then it is always referred to by a reference.

The reference and the (referenced) value are like the 2 sides of the same coin. But the variable and the value (owned by the variable) are dissociable. Indeed, the (ownership over a) value can be moved from a variable to another. That's why the scope of a variable is not (always) the lifetime of the value (temporary) owned by the variable.

point 4: I'd like to distinguish between 3 things

  • (the) "borrowed" (value): THE owned value being borrowed (the value is owned by a variable).
  • (the) "borrow" (reference): THE (unique) reference that refers to a "borrowed" (value) <=> &value (value being the variable that temporarily owns the value).
  • (a) "borrower" (variable): A variable who (temporarily) owns THE "borrow" (the reference that refers to the temporarily owned value). let borrower = &value.

With all that precedes, we can reformulate:

Please note: if we accept the fact that the reference and the (referenced) value are like the 2 sides of the same coin (seen from 2 separate points of view), then they are the "same thing". And thus, as mentioned earlier in this post, we could use the same word. However, using 2 separate words allows us to implicitly specify the adopted "point of view". Specifying the adopted point of view may make things clearer.

// Using Rust terminology: the filefime of the value temporarily owned by the variable "i" outlives the scope of the (run-time) variable "i".

void p() {
    static int i = 0;
    print(i++);
}

What do you think if these formulations ?

Thanks !

I don't really agree with the Book here. They say

lifetime, which is the scope for which that reference is valid

and then immediately give a bunch of examples built around lexical scopes. But lifetimes haven't been restricted to lexical scopes since 2018. Using your terminology, the lexical scope of the borrowed puts a limit on the lifetime of the borrow, that is true -- and that's what the examples are about. But the lifetime doesn't have to be equal to that scope or any other lexical scope.

I wouldn't call a lifetime a scope at all; the possibility of confusion is too high. "Region" is better. (It's still an approximation -- inferred lifetimes are complicated -- but generally good enough for a solid mental model.)

So then...

The liveness of a borrower cannot outlive the borrow lifetime. I.e., the borrower must not be used (or usable) outside of the borrow lifetime. But that liveness region might not correspond to a lexical / drop scope.

The scope of a &T or &mut T variable never really matters. The scope of a struct containing such a borrower can matter, if it has a destructor -- because that destructor will run at the end of the scope.

The scope of borrowed -- the T variable you're creating a &T to -- is an upper limit on the borrow lifetime.


statics aren't variables. And you can have a static reference without the data it points to being in a variable, too. You can also leak data, discarding the owner.

You access the ownerless values by giving it a global name or by reference (or pointer).

I don't really follow your second question. Rust does have "raw pointers" in addition to references.

1 Like

Hello Quinedot,

Thank you for these details. I need the digest it :slight_smile:

Denis