Lately I've become enamored with the idea of using fields-in-traits to define "views" onto a struct as well. The idea would be to enable "partial self borrowing".
I gave an example of source code in this post, but the problem usually arises like this:
- You have some struct
MyStruct
with various fields - You have a helper function
fn foo(&self) -> &Bar
that accesses some subset of those fields - You have another helper function
fn mutate_baz(&mut self, bar: &Bar)
that mutates a different (and disjoint) subset of those fields - You want to invoke
self.mutate_baz(self.foo())
, but the borrow checker is unhappy. - Right now, there are two possible fixes:
- break out those subsets of fields into distinct structs and put the methods on those structs (
self.baz_fields.mutate(self.foo_fields.get())
) - inline the two methods (e.g.,
foo
andmutate_baz
) into the caller
- break out those subsets of fields into distinct structs and put the methods on those structs (
- I find the problem is most acute in between private methods, but it can arise in public interfaces too -- e.g., it affects collections where you want to enable access to distinct keys (you can view
split_at_mut
as being a sort of solution to this desire for slices), though that obviously has complications.- In general though in a public interface you will want the ability to check and document the fact that methods can be invoked separately.
Anyway, the goal here would be that one can solve this by problem by declaring (somehow!) that those methods (foo
and mutate_baz
) operate on disjoint sets of fields. But how to do that? One idea was to leverage fields-in-traits and use those traits to define views on the original struct. I'll sketch the idea here with let
syntax:
struct MyStruct {
field_a: A,
field_b: B,
field_c: C,
field_d: D,
}
trait View {
let field_a: A; // not declared as `mut`
let mut field_b: B; // declared as `mut`.
}
impl View for MyStruct {
let field_a = self.field_a;
let mut field_b = self.field_b;
}
Now I could create a view like this:
impl MyStruct {
fn foo(&mut self) {
let view: &mut dyn View = &mut *self;
...
}
}
Under the base RFC, this is two operations: we create a pointer (self
) of type &mut MyStruct
, then we coerce that into a trait reference (as usual). But we could think a more "composite operation" that the borrow checker is more deeply aware of: that is, a kind of borrow where the result is not a &mut MyStruct
that is then coerced, but rather where the result is directly a &mut dyn View
. In that case, the borrow checker can understand that this borrow can only affect the fields named in the view. This means that we can then permit other borrows of the same path for different views, so long as those views are compatible.
Once we've defined the views, you can imagine using them in the self
like so, fn mutate_bar(self: &mut BarView)
. This is an obvious case where the borrow-checker can make self.mutate_bar(...)
use this more limited form of borrow.
Things I don't love about using traits for this:
- You have to impl them, and presumably there are some restrictions on the traits/impls so that we can identify the fields that are affected.
&mut dyn View
instead of&mut View
Things I do love:
- I like having named views because they are intuitive and can be documented and part of your public API if you really want.
- We can maybe also check that they access disjoint sets of field, though I think the current RFC doesn't quite address this need.
- This feels like a pretty clean and comprehensible mechanism, even if we layer some sugar on top.