Fields in Traits

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 and mutate_baz) into the caller
  • 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.
7 Likes