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!