Clarify the meaning of binding two references to differently scoped referents to the same lifetime in a function signature
Asked Answered
P

3

9

I've been trying to get my head around the Rust borrowing and ownership model.

Suppose we have the following code:

fn main() {
    let a = String::from("short");
    {
        let b = String::from("a long long long string");
        println!("{}", min(&a, &b));
    }
}

fn min<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() < b.len() {
        return a;
    } else {
        return b;
    }
}

min() just returns a reference to the shorter of the two referenced strings. main() passes in two string references whose referents are defined in different scopes. I've used String::from() so that the references don't have a static lifetime. The program correctly prints short. Here is the example in the Rust Playground.

If we refer to the Rustonomicon (which I appreciate is a work in progress doc), we are told that the meaning of a function signature like:

fn as_str<'a>(data: &'a u32) -> &'a str

means the function:

takes a reference to a u32 with some lifetime, and promises that it can produce a reference to a str that can live just as long.

Now let's turn to the signature of min() from my example:

fn min<'a>(a: &'a str, b: &'a str) -> &'a str

This is more invloved, since:

  • We have two input references.
  • Their referents are defined in different scopes meaning that they are valid for different lifetimes (a is valid longer).

Using similar wording to the quoted statement above, what does the function signature of min() mean?

  1. The function accepts two references and promises to produce a reference to a str that can live as long as the referents of a and b? That feels wrong somehow, as if we return the reference to b from min(), then clearly that reference is not valid for the lifetime of a in main().

  2. The function accepts two references and promises to produce a reference to a str that can live as long as the shorter of the two referents of a and b? That could work, since both referents of a and b remain valid in the inner scope of main().

  3. Something else entirely?

To summarise, I don't understand what it means to bind the lifetimes of the two input references of min() to the same lifetime when their referents are defined in different scopes in the caller.

Persona answered 15/3, 2017 at 10:53 Comment(0)
B
4

It's (2): the returned reference lives as long as the shorter input lifetime.

However, from the perspective of the function, both input lifetimes are in fact the same (both being 'a). So given that the variable a from main() clearly lives longer than b, how does this work?

The trick is that the caller shortens the lifetime of one of the two references to match min()s function signature. If you have a reference &'x T, you can convert it to &'y T iff 'x outlives 'y (also written: 'x: 'y). This makes intuitive sense (we can shorten the lifetime of a reference without bad consequences). The compiler performs this conversion automatically. So imagine that the compiler turns your main() into:

let a = String::from("short");
{
    let b = String::from("a long long long string");

    // NOTE: this syntax is not valid Rust! 
    let a_ref: &'a_in_main str = &a;
    let b_ref: &'b_in_main str = &b;
    println!("{}", min(&a as &'b_in_main str, &b));
    //                    ^^^^^^^^^^^^^^^^^^
}

This has to do with something called subtyping and you can read more about this in this excellent answer.

To summarize: the caller shortens one lifetime to match the function signature such that the function can just assume both references have the same lifetime.

Benzene answered 15/3, 2017 at 11:16 Comment(4)
Great answer! Thanks I'll wait until the end of the day to see if anyone else pipes up.Persona
On another note, I wonder if I should try to raise a PR to the Rustonomicon adding this very example. What do you think? It would have saved me some time for sure.Persona
@EddBarrett: I think the maintainers would be delighted to have more contributions, especially from beginners since beginners are the best suited to point out what is a hurdle to them. You may want to open an issue first, to discuss your idea about developing this undercovered topic: this way you can sound them out without first investing too much time, and they can direct your work before you start (maybe they'd rather have a new chapter for more advanced example? maybe they'd rather put it before this example but after this other one? ...).Casi
Hmm, I see it the opposite way around - I'll add an answer.Strickle
S
2

I'm going to go for (3) something else!

With your function signature:

fn min<'a>(a: &'a str, b: &'a str) -> &'a str { ...}

// ...
min(&a, &b)

the 'a is not the lifetime of the objects being borrowed. It is a new lifetime generated by the compiler just for this call. a and b will be borrowed (or possibly reborrowed) for as long as needed for the call, extended by the scope of the return value (since it references the same 'a).

Some examples:

let mut a = String::from("short");
{
    let mut b = String::from("a long long long string");
    // a and b borrowed for the duration of the println!()
    println!("{}", min(&a, &b));
    // a and b borrowed for the duration of the expression, but not
    // later (since l is not a reference)
    let l = min(&a, &b).len();

    {
        // borrowed for s's scope
        let s = min(&a, &b);
        // Invalid: b is borrowed until s goes out of scope
        // b += "...";
    }
    b += "...";  // Ok: b is no longer borrowed.
    // Borrow a and b again to print:
    println!("{}", min(&a, &b));
}

As you can see, the 'a for any individual call is distinct from the lifetime of the actual a and b which are borrowed, though of course both must outlive the generated lifetime of each call.

(Playground)

Strickle answered 15/3, 2017 at 12:33 Comment(5)
I'm on the fence with regard to your answer (though I upvoted it since borrows matter). From the point of view of the callee, I would argue that the borrow does not matter. And thus, from the point of view of the callee, the answer is (2) => the callee guarantee that the result can live as long as 'a, the shorted lifetime. Whether the result actually lives that long or not... doesn't matter. It cannot live any longer, though.Casi
@MatthieuM. If we look from the callee's point of view, there's only one lifetime, so there's not so much to say. (At least from my point of view!)Strickle
What I am trying to say is that there are two ways to look at the question: (1) What is the maximum lifetime that the result can have? and (2) How long are the arguments borrowed for? It seems to me that the OP is angling for (1) and not that interested in (2). I may be wrong, of course, I'm no mind reader!Casi
Ok, I read it as "how are &a and &b squashed into one lifetime 'a. Also not a mind reader!Strickle
That's why I upvoted both yours and Lukas' answers, you both make complete valid point... and I have no idea which the OP wanted... and maybe the OP actually needs both points, because it's not immediately obvious there is a difference to a beginner :)Casi
G
1

Apart from what @Lukas have mentioned in the answer, you can also read the signature of the function as - The returned reference is valid till the point where both the passed references are valid i.e its an conjunction (aka AND) between the parameters lifetime.

There is something more to it. Below are two code examples:

    let a = String::from("short");
    {
        let c: &str;
        let b = String::from("a long long long string");
        c = min(&a, &b);

    } 

AND

let a = String::from("short");
    {
        let b = String::from("a long long long string");
        let c: &str;
        c = min(&a, &b);

    }

The first one doesn't work (second one does). It may seem that both b and c have same lifetime as they are in same scope but the ordering in the scope also matters as in the first case b lifetime will end before c.

Grazynagreabe answered 15/3, 2017 at 12:26 Comment(1)
Thanks! In fact I think c and b are in different scopes in the eyes of the compiler, as each let binding makes a new implicit scope (according to Rustonomicon).Persona

© 2022 - 2024 — McMap. All rights reserved.