A few more thoughts on the topic that I’m having.
In the context of a struct
struct Foo {
bar: u8,
baz: u8,
}
First, one could discuss whether a view type such s &{baz} Foo is
- a special / new kind of type by itself
- a “regular” reference type, so it’s a special case of
&T where T is a new type “{baz} Foo”
The code
pub type GoldenTicket = {serial_number, mut owner} GoldenTicketData;
in the post hints towards the latter approach.
Notably however, this approach would not interact nicely with the way references work in Rust:
Assuming that {baz} Foo would be a Sized type, you could do things like mem::swap on two &mut {baz} Foo instances; the way mem::swap operates is (AFAIK) that it memcopies the whole value including padding, and in the case of {baz} Foo this would be including the contents of the bar field.
I think we might actually have no such problem for shared references, provided that a view-type like {baz} Foo never implements Copy. So &{baz} Foo is probably fine.
This reminds me a bit of pinning. It would probably be sound to work with Pin<&mut {baz} Foo> only instead of &mut {baz} Foo; for this, {baz} Foo would be an !Unpin type without structural pinning, i.e. offering a Pin<&mut {baz} Foo> -> &mut u8 conversion for accessing the baz field. Using the existing Pin for this would be a bit weird; let's give a new name to this, I’ll temporarily choose “NoMove”. You could create a NoMove<&mut {baz} Foo> reference directly for a local variable foo: Foo on the stack (but you could not create a &mut {baz} Foo reference!), and you could project NoMove<&mut {baz} Foo> -> &mut u8. You could also split-reborrow a &mut Foo into NoMove<&mut {bar} Foo> and NoMove<&mut {baz} Foo>. The new wrapper could also be combined with Pin, allowing Pin<&mut Foo> to be split into Pin<NoMove<&mut {bar} Foo>> and Pin<NoMove<&mut {baz} Foo>>.
As an alternative to a new wrapper, &mut {...path} S could be considered something different from &mut T with T == {...path} S; or a new trait like Sized could be introduced that view-types like {bar} Foo don't implement, and that mem::replace and similar functions require.
Or perhaps just making {bar} Foo be considered an unsized (i.e. !Sized) type could make sense? It would be the first “unsized” type where &{bar} Foo has no meta-info, i.e. size_of::<&{bar} Foo>() == size_of::<usize>()
I’m just realizing that in a lot of places above, I should probably have written &mut {mut bar} Foo instead of &mut {bar} Foo. I’ll stick with the latter below, too, though for simplicity.
Relating &{...path(s)} T to &T, I think the question of whether e.g. &Foo is the same as &{bar, baz} Foo comes up. And similarly for &mut.
It would probably simplify the view-type system if &Foo was just a “syntax sugar” equivalent to &{bar, baz} Foo (i.e. listing all the fields). There’s however the question of empty lists, in particular with mutable references:
- First of all,
&mut {} Foo doesn’t make much practical sense, so it’s unclear if it should be allowed or disallowed in the first place. If it’s allowed, it could probably be duplicated: you could split &mut {} Foo into &mut {} Foo and &mut {} Foo similar to how you could split &mut {bar, baz} Foo into &mut {bar} Foo and &mut {baz} Foo.
- For structs
Bar with no fields, currently &mut Bar is still in some sense exclusive. It might be possible that some existing API somehow depends on that that’s the case, although I’m not actually sure if that’s really possible. I’m not talking about #[non_exhaustive] fieldless structs here (yet). For exhaustive fieldless structs, all fields are public, so anyone can just create new instances of them (provided they can name the type, I guess...) if they want to get hold lots of &mut ... references at the same time. Still, it somehow feels weird/questionable to just start allowing duplication of mutable references to field-less zero-sized types.
About #[non_exhaustive] structs: It might make sense for those to have some way of indicating complements. For those types, &{list, of, all, fields} Type should not be the same as &Type, because the list might not stay exhaustive in the future. Still, it can make sense to want to split up &mut Type into &mut {field} Type and &mut {..everything-but field} Type. Let me use temporary syntax ~{field} to refer to everything but the field "field". So now you can split &mut Type into &mut {field} Type and &mut ~{field} Type. For ordinary exhaustive structs like Foo above then, &{baz} Foo would be the same as &~{bar} Foo; for a non-exhaustive struct there’s always all-remaining-and-future fields that are not part of &{list, of, fields} Type, but are part of &~{list, of, excluded, fields}; hence for those &{…} Type and &~{…} Type are always different. Finally, &Type would still be syntactic sugar; now for &~{} Type.
Actually, this syntax does not give a way to specify whether all-remaining-and-future fields are borrowed mutably or immutable; I don’t have a great idea how to incorporate this.
How this interacts with "longer places": in a struct like
pub struct Foo {
pub bar: Bar,
}
pub struct Bar {
pub x: u8,
pub y: u8,
}
it would make sense that &Foo is the same as &{bar} Foo and the same as &{bar.x, bar.y} Foo.
However by the same token, for
pub struct Foo {
pub bar: Bar,
}
pub struct Bar {}
now &Foo is the same as &{bar} Foo and the same as &{} Foo? But – at least when bar would be private – unlike for truly field-less structs, I’d argue it is not sound anymore to be able to duplicate &mut Foo references. I’d say that
pub struct Foo {
bar: Bar,
}
pub struct Bar {}
should behave the same way as
#[non_exhaustive]
pub struct Foo {
bar: Bar,
}
pub struct Bar {}
!!
But [non_exhaustive] fields need the extra all-remaining-and-future-fields place to be considered, you can only have either &mut Foo be the same as &mut {} Foo or have them not be the same, and it’s also somewhat questionable to have this depend on how public Foo’s fields are. Maybe then it’s better when
&Foo is the same as &{bar} Foo and the same as &{} Foo
is not true after all, even for the case where all fields are public. What exactly is true and not true about this statement though? And is in the example before that the statement
&Foo is the same as &{bar} Foo and the same as &{bar.x, bar.y} Foo
still true? I don’t know the best answer here.
Synonyms / named sets of fields: Those are a must in order to support private fields. It’s probably also necessary to be able to declare sets of pairwise disjoint sets of places. E.g. if I have a type
// all fields private
pub struct Matrix3Times3 {
x_1_1: f32, x_1_2: f32, x_1_3: f32,
x_2_1: f32, x_2_2: f32, x_2_3: f32,
x_3_1: f32, x_3_2: f32, x_3_3: f32,
}
and I want to provide view-types
pub type Matrix3Times3Row1 = {x_1_1, x_1_2, x_1_3} Matrix3Times3;
pub type Matrix3Times3Row2 = {x_2_1, x_2_2, x_2_3} Matrix3Times3;
pub type Matrix3Times3Row3 = {x_3_1, x_3_2, x_3_3} Matrix3Times3;
as well as
pub type Matrix3Times3Colum1 = {x_1_1, x_2_1, x_3_1} Matrix3Times3;
pub type Matrix3Times3Colum2 = {x_1_2, x_2_2, x_3_2} Matrix3Times3;
pub type Matrix3Times3Colum3 = {x_1_3, x_2_3, x_3_3} Matrix3Times3;
Then you could split &mut Matrix3Times3 into &mut Matrix3Times3Row1, &mut Matrix3Times3Row2 and &mut Matrix3Times3Row3. Or you could split &mut Matrix3Times3 into &mut Matrix3Times3Colum1, &mut Matrix3Times3Colum2 and &mut Matrix3Times3Colum3. But having the compiler determine this automatically would leak implementation details: It’s probably better to have the possibility (and requirement) to declare e.g.
pairwise_disjoint_view_types!{ of Matrix3Times3 {
Matrix3Times3Row1,
Matrix3Times3Row2,
Matrix3Times3Row3,
}}
pairwise_disjoint_view_types!{ of Matrix3Times3 {
Matrix3Times3Colum1,
Matrix3Times3Colum2,
Matrix3Times3Colum3,
}}
and only allow splitting a borrow of all the private fields of a struct into multiple subsets if those subsets are explicitly declared to be disjoint. (At least in code where the private fields really are not visible.)
This also makes sense for traits. If you have some way of providing associated-view-types Bar and Baz in a trait; users of this trait might want to split up &mut Self into &mut Bar and &mut Baz; but for this the trait would need to (be able to) specify/require the two view-types to be disjoint!