A little lifetime exercise (for newbies)

Challange: replace the question marks in the following function signature:

fn func<?>(a: &? i32, b: &? i32) -> &? i32

with someghing that does not contain the mut keyword, to make both

{
    a
}

and

{
   b
}

are valid definition body of the function.

There will be two answers, one is easy and one is harder.

Edited for better wording.

4 Likes

use the same lifetime for all of them?

fn func<'a>(a: &'a i32, b: &'a i32) -> &'a i32 ...

One valid answer. But not the one I want to see :slight_smile:

hmm, then that means the answer is something I don't understand about lifetime yet (is pulling hair on the exercism puzzles that involves lifetimes)

Did you mean to use

fn func(a: &'a i32, b: &'b i32) -> &i32;

as the starting point?

Actually, I want to give a complete analysis of this later. :slight_smile:

Tips : I believe there are 4 valid answers... No I think there is only 2.

Another tips: I cannot write

fn func(a: &'a i32, b: &'b i32) -> &i32;

as the starting point, because you have to have the lifetime declared in the generic parameter part <>. But the generic part is where you will play the magic...

Sure, you did after all write "illegal" function signature, so it should not be a problem to leave out the declaration part :slight_smile: But since you weren't after @Jeffrey04's answer, you're probably after different lifetimes for a and b, and should probably state that so beginners don't get confused.

Ok, I just thought of the remaining 2 options, so you're right not to demand different lifetimes for a and b.

There are over 4 valid, along a very similar stream. (No comment about useful.)

Now you have changed the question will post now rather than later what is to be likely best for all to use;

fn func(a: i32, b: i32) -> i32

It is similar to the clippy

Well, your code didn't have the & symbol, it shouldn't count as I only mention to replace the "question mark"...

But wait, I think I have to limit the use of mut otherwise the number of answers is uncontrollable...

Hmm...are you looking for:

fn func( a: &'a i32, b: &'b i32 ) -> &'c i32 where 'a : 'c, 'b : 'c
2 Likes

Yes... This is it.

The answer that @Jeffrey04 gave above should be the answer you want to see :slight_smile: - it's the canonical way to represent the situation (although using &i32 is not very useful - &str would be better, for example).

I'm afraid the other valid combinations you're going to propose/elicit are only going to confuse beginners because there's no reason to use them here. If you want to demonstrate more "exotic" generic lifetime bounds, it's always better to show them in the context where they're actually required, and then explain why other approaches don't work.

4 Likes

This is what came to my mind, similar to @gbutler69's solution but more strictly holding to "only replace the ?s":

fn func<'a: 'b, 'b>(a: &'a i32, b: &'b i32) -> &'b i32

But variance makes it equivalent to the first solution, so 'b is just noise here. The "three different lifetimes" solution is no better.

I also thought of another family of solutions, along these lines:

fn func<'a>(a: &'a i32, b: &'static i32) -> &'a i32

This could actually be a useful thing to do on rare occasions, like if func does something like caching b behind the scenes.

1 Like

I took this as an exercise to demonstrate unusual (not necessarily idiomatic or correct) usages of life-time constraints to see some interactions in the various ways of declaring lifetimes and how various combinations can end up equivalent.

That's what it ends up being, but it shouldn't have "for newbies" in the title then :slight_smile:. And I don't think it's a useful learning/illustration example because it's just more verbose ways to do the same thing that can be accomplished in canonical and shorter code.

A newbie reading this doesn't learn anything from the more elaborate bounds - they wouldn't necessarily figure out how to take this example and translate it to a situation where those types of constraints are actually necessary to pass borrowck.

Sorry, I don't mean to be a downer on this - I appreciate @earthengine trying to be helpful to folks here - but I feel that this effort can be better spent on proper illustrations/examples.

2 Likes

Although I mostly agree I do see one way in such a demonstration can be useful for newbies, if we use the canonical solution (leaving aside that you may as well dispense with references in this case):

fn func<'a> ( a: &'a i32, b: &'a i32 ) -> &'a i32;

or the less idiomatic solution:

fn func<'a, 'b : 'a> ( a: &'a i32, b: &'b i32 ) -> &'a i32;

When you try to explain what is going on here a newbie will tend to think in terms of, "lifetime 'a exists, lifetime 'b is longer than lifetime 'a, the thing we're returning has lifetime 'a so how could b ever be the return value". They don't immediately understand that 'a : 'b is equivalent to 'b : 'a in this case and have trouble understanding that the return value must simply not outlive a or b. That's all the declaration says.

With the 3 lifetime solution and where clause, it is apparent that the thing being returned has to outlive both a and b in all cases because it is explicitly stated. Once they understand that, I believe it could be easier for them to then understand the simpler, more idiomatic version and why that simpler version is also correct.

That being said, I'm not sure that that analysis is correct as far as how a newbie might see this.

4 Likes

IMO, the rust book has a better (i.e. useful) example of a necessary outlives relationship: Advanced Lifetimes - The Rust Programming Language.

More examples along those lines would be useful. Again, just IMO.

1 Like

@gbutler69

Thank you for you fantastic analysis! You saved me a lot.

I just want to say how I come up with this. Recently in my projects when I started to twist the lifetimes, I found my self have to decide the "longest" or "shortest" lifetime (more often) from the two input values, to make sure my generic functions as generic as it can be.

The thing I want to express to newbies is, when given any two lifetimes, there would always exist a lifetime that are greater/less then both.

For experienced users though, I also want to say something: the 3 lifetime form is the most generic form. Code that allowed by any other annotations, are subsets of it. Although I don't have proof yet, in theory for any function signature there should exist exactly one such a form.

The reason we do not infer this form automatically, is not because it is not possible, it is for function signature readability.

For the language, I think we should think of some language level facility to express the LSB lifetime of a type - in general, a type can have multiple lifetime parameters, but if we can talk about the shortest one, we are talking about the guaranteed scope that will ensure the type is valid, which will be very useful in generics. Right now, this example is a demonstrate of how to achieve this in today's language.

hmm #offtopic so this forum is more active at this hour eh~

Edit: the discussion is interesting to a newbie like me (:

It seems to vary quite a lot, but, I do see a lot of early morning (WRT US/NewYork TZ) activity.