Having mutability in several views of a struct


#1

I have been thinking the following idea for some time and I thought I would share it here, since I have not seen discussed elsewhere.

The idea is to be able to split a struct into views such that diferent functions have limited access to the fields. And more importly we could split mutable access to a struct into mutable accesses to disjoint views of the struct.

I think the following code shows this clearly

struct Big
{
	x: usize,
}

struct Composite
{
	a: Big,
	b: Big,
	c: Big,
	d: Big,
}

//We declare different views of the struct
view CompositeAB for Composite(a,b);
view CompositeAC for Composite(a,c);
view CompositeAD for Composite(a,d);
view CompositeBC for Composite(b,c);
view CompositeBD for Composite(b,d);
view CompositeCD for Composite(c,d);

//Each view could have their own implementations
impl CompositeAB
{
	fn compute(&mut self)
	{
		self.a.x+=self.b.x;
		self.b.x*=self.a.x;
		self.a.x+=self.b.x;
	}
}

impl CompositeAC
{
	fn compute(&mut self)
	{
		self.a.x=(self.a+1)*(self.c.x+1);
		self.c.x=(self.a+2)*(self.c.x+2);
	}
}

impl CompositeAD
{
	fn compute(&mut self)
	{
		self.a.x=(self.a+2)*(self.d.x+1);
		self.d.x=(self.a+3)*(self.d.x+2);
	}
}

impl CompositeBC
{
	fn compute(&mut self)
	{
		self.b.x=(self.b+3)*(self.c.x+1);
		self.c.x=(self.b+4)*(self.c.x+2);
	}
}

impl CompositeBD
{
	fn compute(&mut self)
	{
		self.b.x=(self.b+4)*(self.d.x+1);
		self.d.x=(self.b+5)*(self.d.x+2);
	}
}

impl CompositeCD
{
	fn compute(&mut self)
	{
		self.c.x=(self.c+5)*(self.d.x+1);
		self.d.x=(self.c+6)*(self.d.x+2);
	}
}

fn main()
{
	let mut thing = Composite::new();
	{
		//We could get mutable references to disjoint views
		let ab = CompositeAB(&mut thing);
		let cd = CompositeCD(&mut thing);
		crossbeam::scope( move |scope| {
			scope.spawn(move || {
				ab.compute();
			});
			scope.spawn(move || {
				cd.compute();
			});
		});
	}
	{
		//To another combination of views
		let ac = CompositeAC(&mut thing);
		let bd = CompositeBD(&mut thing);
		crossbeam::scope( move |scope| {
			scope.spawn(move || {
				ac.compute();
			});
			scope.spawn(move || {
				bd.compute();
			});
		});
	}
	{
		//The third combination
		let ad = CompositeAD(&mut thing);
		let bc = CompositeBC(&mut thing);
		crossbeam::scope( move |scope| {
			scope.spawn(move || {
				ad.compute();
			});
			scope.spawn(move || {
				bc.compute();
			});
		});
	}
	{
		//This would be an error because of overlap
		//let ab = CompositeAB(&mut thing);
		//let ac = CompositeAC(&mut thing);
	}
}

In most cases, there is only one interesting combinations of views, case in which it can be done just by including a struct as field. But when there are multiple views of interest it is not clear the implementation with the current language. I also realize that this idea could have a too narrow use case, but it could be worth discussing.


#2

This is definitely hitting at a real problem, specifically the need to have helper functions that touch disjoint sets of fields which can overlap.

This previous thread also discussed the same general topic:

The idea of explicit views is interesting; as I mention in that thread, I’ve been leaning towards the idea that it would be nice – at least for private functions – for the compiler to do the inference for you. But I’ve been wanting some kind of explicit syntax as well.

I actually do something sort of like explicit views occassionally in my own coding. That is, I create “shadow structs”, like:

struct RealStruct {
    foo: Foo,
    bar: Bar,
    baz: Baz,
}

struct ShadowStruct<'a> {
    foo: &'a Foo, // foo is immutable
    bar: &'a mut Bar, // bar and baz are not
    baz: &'a mut Baz,
}

#3

Well, I saw the problem mostly in a theoretical way. If you have had some occasions in which this is relevant maybe it needs to be addressed. Because having to make a struct with references to each field of other struct seems pretty bad, both in efficiency and ergonomics. In contrast, using a view would be in low level a single pointer to the real struct. Its only problem being that it requires new notation.

Regarding the notation for creating the view from a struct there are several possibilities.

struct Real{...}
view Part for Real(...);

let real_thing: Real;

//This is my original attempt, which does not seem very natural
let part_thing = Part(&mut real_thing);

//I like this notation more
let part_thing = &mut (real_thing as Part);

//Or maybe this, since references should be able to be 'viewed'
let part_thing = (&mut real_thing) as (&mut Part);

And following your example, perhaps the fields in a view should be inmutable by default. Thus we could write

struct RealStruct{
	foo: Foo,
	bar: Bar,
	baz: Baz,
	other: OtherData,
}
view ShadowStruct for RealStruct(foo, mut bar, mut baz);

Then an inmutable ShadowStruct could access immutably to foo, bar and baz, but not to other. And a mutable ShadowStruct could access to foo inmutably and to bar and baz mutably.

The original example in the other thread could be written as follows.

struct Foo {
	x: u32,
	v: [u32; 2]
}

view FooBar for Foo(mut x);

impl Foo {
	fn new() -> Self {
		Foo { x: 0, v: [0; 2] }
	}

	fn spam(&mut self) {
		for _ in self.v.iter() {
			//Legal since v and FooBar are disjoint
			(self as &mut FooBar).bar();
		}
	}
}

impl FooBar {
	fn bar(&mut self) {
		self.x += 1;
	}

}

#4

Hmm, so let me bring in a bit more context. The main time that I think this problem strikes people is sort of mundane:

pub struct MyStruct {
    some_data: Data,
    some_more_data: MoreData,
}

impl MyStruct {
    pub fn some_function(&mut self) {
        let ref1 = self.ref_to_some_data(); // borrows self
        self.modify_some_more_data(ref1); // error, self is still borrowed
    }

    fn ref_to_some_data(&self) -> &u32 {
        &self.some_data.some_field
    }

    fn modify_some_more_data(&mut self, ref: &u32) {
        self.some_more_data.modify(ref)
    }
}

Here the key point is that ref_to_some_data and modify_some_more_data access disjoint fields. (As I wrote earlier, I would like code this to “just compile” – with no syntactic changes – but let’s discuss that a bit later and focus instead on how you would write out something more explicit if you wanted to.)

I was thinking that maybe “views” might be that you get into via coercions or casts. Basically a &mut Foo could be coerced to a &mut FooView, where FooView is some view on Foo that selects a subset of its fields. Similarly, borrows like &mut foo would have not only a lifetime 'a of the borrow, but a view V that is being borrowed, so that the final type is &'a mut V.

(Note that we don’t presently allow you to write the lifetime of a borrow explicitly; we probably wouldn’t let you write the lifetime of a view either, but instead use context, meaning you might do something like this to select a view explicitly:

let x: &mut View = &mut foo
(&mut foo) as &mut View

Most of the time, though, I imagine you’d be getting your view via the self type of a method that is being called:

fn foo(self: &mut View, ...) { ... }

and when we insert the “autoref” for x.foo(), we would insert one with the appropriate view.


Fields in Traits
#5

Oh, a few more thoughts. A view I imagine might be just a subset of the fields with either mut or not in front of them. This then makes me think of the fields-in-traits RFC, I wonder if a “view” could just really be a trait.


Fields in Traits
Fields in Traits
#6

Well, with this mundane example one can just rewrite it.

pub struct MyStruct {
	some_data: Data,
	some_more_data: MoreData,
}

impl MyStruct {
	pub fn some_function(&mut self) {
		let ref1 = &self.some_data.some_field;
		self.some_more_datat.modify(ref1);
	}
}

There are more complicated examples for which doing this transform is impossible, as the one in my first post. But as these are artificial examples I wonder if there is actual need for this.

I slightly disagree. I prefer the code to be explicit, otherwise the errors could get quite confusing.

It seems quite interesting and I do not view big complications.

struct Foo {
	x: u32,
	v: [u32; 2]
}

trait FooBar
{
	mut x:u32,//mut here feels a little weird. But how else to make correctly the ShadowStruct example?
	fn bar(&mut self) {
		self.x += 1;
	}
}

impl FooBar for Foo
{
	x: self.x,
}

impl Foo {
	fn new() -> Self {
		Foo { x: 0, v: [0; 2] }
	}

	fn spam(&mut self) {
		for _ in self.v.iter() {
			//Legal since v and FooBar are disjoint
			self.bar();
		}
	}
}

The mut bit is annoying. But it is useful for this example.

struct Real{
	foo: Foo,
	bar: Bar,
	baz: Baz,
}
trait R
{
	foo: Foo,//inmutable
	mut bar: Bar,
	fn compute(&mut self){...}
}
trait Z
{
	foo: Foo,//inmutable
	mut baz: Baz,
	fn compute(&mut self){...}
}
impl R for Real
{
	foo: self.foo,
	bar: self.bar,
}
impl Z for Real
{
	foo: self.foo,
	baz: self.baz,
}
impl Real
{
	fn compute(&mut self)
	{
		//This is legal because the intersection of R and Z is foo, which is always used as inmutable
		let r=self as &mut R;
		let z=self as &mut Z;
		//Call r.compute(...) and z.compute(...) in parallel or whatever
	}
}

I see you have been discussing traits with fields for quite a long time, while I have been using Rust for just a few weeks. So you should know better which problems there are to adopt this.


#7

Changing this to non-trivial case:

fn main() {
   let my = MyStruct::new();
   let ref1 = my.ref_to_some_data();
   my.modify_some_more_data(ref1);
}

The equivalent fix would require making the fields public:

   let ref1 = &my.some_data;
   my.some_more_data.modify(ref1);

And in practice I find this unacceptable, because it breaks encapsulation. It also allows

my.some_data = new_object

which may break invariants that the implementation needs to uphold.

Because of that I often wished I could expose fields as read-only:

pub struct MyStruct {
    pub(but read-only) some_data: Data,
    pub(but read-only) some_more_data: MoreData,
}

#8

So, first, hold-up. I was very specific in my initial example to use private functions: the majority of time that I hit this problem, it concerns internal details of a type. Basically, I would prefer to have some helper functions, but I cannot, because the borrow checker doesn’t let me. This – the private function case – is I think a distinct use case worth considering separately when it comes to ergonomics (maybe not the underlying mechanism).

I find that when I am working with types “externally”, as in your example, it is much more unusual for me to have the insight into “what fields a method may touch”. I tend to treat it as an atomic unit, and I don’t find the rules prohibitive (now, maybe it influenced the author of the type in terms of the public API that they export).

There are some exceptions: the main ones that I’ve noticed involve collections. For example, I like split_at_mut on vectors, which covers more-or-less this same case. Similarly, I sometimes want the ability to have multiple mutable refs into a hashmap.

All of this is not to say that we don’t need to worry about external users at all – but I would say that for an external user, defining an explicit view or trait is not necessarily prohibitive. It makes sense because it is giving your users a “window” into your “mental model”. i.e., if you have a struct Connection that exposes two views, Buffer and Address, then you are telling them they can think of a connection has having those subparts, and then you may have distinct methods that sometimes operate only on the buffer, sometimes only on the address, etc.

Anyway, I am pretty excited about the idea of leveraging fields in traits for this. (Don’t have time to write a bunch here, more later.)


#9

Re-reading this, I don’t think it fully communicated my intent. First, if possible, I certainly prefer a mechanism that acts the same for internal/external cases. Leaning on privacy always feels like a bit of a smell.

But secondly, part of what I meant to say here was that it’s very important that you be able to “fully encapsulate” these views so that they are not visible in your public API. (But I don’t think using traits to describe them is incompatible with that.)

(But circling back to the first point, if we were going to try and make something work semi-automatically – e.g., inferring views – I would definitely limit it to the private fn case. But I’m happy to shelve all talk of inference for now and focus on explicit notation. I suspect that would not be too onerous, this doesn’t arise that frequently.)