Generalized Partial Borrows

In the original proposal I specifically talked about this:

fn foo(value: &(u8, '! u8, '! u8)) {}

Or, for clarity:

fn foo(value: &('_ u8, '! u8, '! u8)) {}

I think you misunderstood me - I was talking about tuple references, not references to tuples.

Tuple reference:

&('x, '!, '!) Foo  // equivalent to `&{0: 'x, ..} Foo`
&(, '!, '!) Foo  // equivalent to `&{0, ..} Foo` and `&('_, '!, '!) Foo` and `&('_, ..) Foo`

V.s. reference to tuple:

&('x, '!, '!) (T, U, V)  // equivalent to `&('x, ..) (u8, u8, u8)` and `&('x T, '! U, '! V)`

If we're talking about owned values - sure. References are a different thing, they require lifetimes to be safe and sound, and so do views (especially shared ones with disjoint mutable accesses).

The issue here is whether we make the lifetimes (or rather, lifespans) explicit in the syntax or not. I argue that since they are for regular references, they should also be for partial ones.

this one means you almost have to read the type + lifetime backwards to know where x comes from

The .{...} syntax has the same issue, if you think about it. Go figure what ref group this particular field belongs to. Both variants suffer from this, which leads me to believe that it is either unavoidable or can be improved for both.

I think a soft keyword would actually be fine here, since struct field names are just identifiers and not expressions or patterns.

Take the following function:

fn foo(val: &mut Foo.{grp1, &grp2}) -> &bool {...}

fn bar(val: &mut Foo.{&grp1, grp2}) -> &u64 {...}

How does the compiler know which ref group is being borrowed? If it assumes both are, that is overly conservative and would not allow calling bar after foo (and vice versa).

While I'm at it, the immutable reference syntax &mut Foo.{&imm_grp, ...} is kinda clunky - I think mutability as opt in would be better:

fn copy_y_get_x_mut(val: &Struct.{mut x, y}) {...}  // instead of `&mut Struct.{x, &y}`
1 Like

True. I'd assumed they wouldn't differ because they're both from the same struct, but you're right. We probably want/need the ability to specify which group the return value comes from => Lifetimes in the field list => Having it before the type like you suggested probably makes more sense then.

fn foo(_: &{x, mut y} Foo) -> &u8 {} // Same as second line (lifetime elision)
fn foo<'a>(_: &'a {x, mut y} Foo) -> &'a u8 {} // return value can come from x or y
fn foo<'a, 'b>(_: &{'a x, 'b mut y} Foo) -> &'a u8 {} // return value comes from x

The more I think about having the fields first the more I like it: "I need a reference to the fields {x and mut y) of Foo".

1 Like

To be pedantic - not fields but rather named lifespans (or by indices or place if it's a tuple-like fragmented reference). Analogous to the ref_groups mentioned earlier.

I think I have a solution:

The original proposal included the syntax struct ref &('a, 'b, ...) Struct {...} because the lifetimes 'a, 'b, ... are like generic parameters within (...) braces instead of <...> to distinguish them from the syntax for generic parameters of structs and functions. One could argue in favor of

fn foo(_: &<'_, '_ mut> Struct) {...}

Instead of

fn foo(_: &('_, '_) Struct) {...}

But for me it's simply not esthetic.

The problem with this "generic parameter" approach is, as was mentioned multiple times, the lack of named arguments. I hence propose we embrace the generic syntax fully and allow default lifespans, e.g.

struct ref &(~x='!, ~y='!, ~priv='!) Struct {...}  //'

fn foo(_: &(~x='_) Struct) {...}  // similar to `StructWithDefArg<T = i32>`

Or

struct ref &<~x='!, ~y='!, ~priv='!> Struct {...}  //'

fn foo(_: &<~x='_> Struct) {...} 

Now a new problem appears - what does &Foo represent? A regular reference with all lifespans being '_ or a fragmnted reference with the default lifespans being '!? The only solution to this problem I can think of right now is requiring at least one lifespan to not have a default value. Or maybe allow lifespans to be '_ by default and if elision doesn't work demand the lifespan be specified.