Lifetime operator as enclosing operator

As already discussed here:

I think the current lifetime notation is both unintuitive and inpractical. It forces a separation of the reference operator from the type identifier, what makes code hard to read.

I suggest using diamonds as replacement, as they are known, but other enclosing symbols would still fix the problem.

Current:

fn foo<'a, 'b, T>(&'a mut self, bar: &'b T) -> &'b T

Suggested:

fn foo<a, b, T>(&<a>mut self, bar: &<b>T) -> &<b>T

To avoid compatibility breaking changes, the aposthrophe notation can be kept for a first, even though I'd suggest making the enclosing operator official coding standard.

I am confused: how exactly is this substantially different from the current syntax? The reference operator is still separated from the type identifier, just with different symbols interjected (even assuming that is a problem in the first place, which I don't think it is).

Furthermore, how does the parser know in fn foo<a, b, T> that a and b are supposed to be lifetimes rather than type parameters?

6 Likes

No space between both. It's visually connected.

It's similiar to common use of the reference operator, without space inbetween.

I see that point. I also though of maybe somehow separating the lifetime identifiers from other generic identifiers. That's also discussed in the linked thread.

Question though: Is that necessary? Does it increase usabillity or improve clarity?

I'd agree on your point. I don't think it's necessary, but it does improve clarity. Maybe using a keyword to determine the kind of generic might be an option. Like:

fn foo<lifetime a, lifetime b, type T>(&<a>mut self, bar: &<b>T) -> &<b>T

There is another problem with this. If i have a type Foo<'a, T=Bar> then Foo<'_> and Foo<_> inside a let binding mean different things

2 Likes

See Frequently Requested Changes.

14 Likes

Interesting page.

I'd suggest linking this (or the parent page) in the editor when a new topic is created.

Hm, okay. I'm actually quite new to the language, so I don't really understand what you mean.

But if I understand you correctly you'd suggest making a clear distinction bewteen lifetimes and other generics in syntax? Like i.e.:

fn foo'a,b'<T>(&'a'mut self, bar: &'b'T) -> &'b'T

You can gain a lot of clarity by using lifetime elision to your advantage:

fn foo<'a, T>(&mut self, bar: &'a T) -> &'a T

This example only needs a single lifetime annotation. Less clutter, more clearly expresses intent.

And the uneven apostrophes were only unfamiliar for the first few days for me. It becomes normal after you get used to it. Like how numerals are written right-to-left but letters and words are written left-to-right (in most languages anyway, there are exceptions). You probably got used to that quirk and it no longer feels out of place (but it still does if you really dwell on it).

5 Likes

Sure you can get used to it. But for me it's still less clear and readable when the reference operator is detached form the identifier it belongs to.

Especially their is an inconsistency arising, cause without a lifetime operator you would not detach the reference operator from its identifier. Means you have:

fn foo<'a, T, V>(foo: &V, bar: &'a T) -> &'a T

My only wish is that they used any other character than ', because most text editors autocomplete the closing quote and I have to delete it. So if anything we could just change ' into something else, like ~.

I do quite like how the current syntax looks tough. &'a T is imo better that &<a> T or &~a T, it's just annoying to write.

1 Like

This bothered me as well when I was new to the language, but I've gotten used to it. Does it help to think of the lifetime annotation as part of the reference operator? That is, the reference operator is &'x, not just &, but sometimes you can leave out the 'x part.

This also bothers me but the problem is we're basically out of ASCII punctuation. The only alternative punctuation characters that don't already have a use are `, which would also be an unpaired quote, and @, which is too large and obtrusive.

(Not a serious comment.)

If it's consistency we're going for, &<'a> Ty is still a special flower because it ejects the type generics from <>. More consistent with other type constructors would be &<'a, Ty>. And it wouldn't be &<'a> mut Ty, it would be &mut<'a, Ty>.

But references aren't the only types with special syntax, so we could go further...

&'a T :arrow_right: &<'a, T>
&'a mut T :arrow_right: &mut<'a, T>
*const T :arrow_right: *const<T>
[T] :arrow_right: []<T>
[T; N] :arrow_right: [;]<T, N>
() :arrow_right: ()
(T,) :arrow_right: (,)<T>
(T,U) :arrow_right: (,,)<T, U>
fn(T, U) -> R :arrow_right: fn<(,,)<T,U>, R>
&'a mut [(T, &'b U)] :arrow_right: &mut<'a, []<(,,)<T, &<'b, U>>>>
5 Likes

A few more things that occurred to me:

I always think of the apostrophe as part of the name, not a separate operator. Inserting whitespace between the apostrophe and the rest of the identifier will cause a parser error. So this may be closer to reality than I recognize.

That would mean 'a is a full token, not two.

I don't follow. That sample contrasts &V and &'a T. But all of the following are valid. The only mandatory whitespace separates the lifetime identifier from the type identifier:

  • & V
  • & '_ V
  • & mut V,
  • & '_ mut V.

It's already consistent.


The other thing is that I would personally be open to the option of full lifetime elision with full body analysis in limited cases (similar to the analysis done for closure captures). It could be opt-in with a nightly feature flag, because it is likely to slow down compilation.

These function signatures could never be used for public interfaces, and that's non-negotiable. But if you are just writing code quickly and want the compiler to do more type inference and you don't care about the downsides, I don't see any harm in that.

#![feature(bikeshed_infer_lifetimes_from_body)]

fn foo<T>(&mut self, bar: &T) -> &T {
    bar
}
1 Like

I already do that :wink:

Though I also see the reference operator as part of the type, what makes things more complicate.

Sure you can place a whitespace between reference operator and type, but that's kinda uncommon and in my opinion that makes reading the code harder. If the reference operator wasn't an operator but a keyword consisting of several chars, than that would be my preferred solution to the problem. Like:

  • ref V
  • ref '_ V
  • ref mut V
  • ref '_ mut V

You can absolutely do that today. (Doesn't mean that you should or that it is really helpful, but that option does exist.)

I think they are saying something more like:

fn foo<V>(bar: ref V) -> ref V {
    bar
}

Having a preferred keyword or operator is a nonstarter. You can have an opinion on the color of the bikeshed, but getting everyone to agree is impractical.

1 Like

That's why I instead suggest modifying the lifetime operator in a reasonable way.

It's still a bikeshed :wink:

Another issue with the proposed syntax is how it would look like in constraints:

... where T: <a>
... where T: Sized + <a>
... where <a>: <b>

That again looks more like type generics than lifetimes to me.

1 Like

I'm open for alternative approaches for the alternative syntax.

These constraints, by the way, look like very weird syntax anyway, even with the unary lifetime operator. Defining a generic type parameter or a lifetime as the addition of a type and a lifetime is very far away from intuitive.

I'm just thinking, maybe it would be better to write a tool that converts a language with extended, less weird syntax to pure Rust syntax.