Partially borrowed/moved struct types


#1

Rust has the notion of borrowing or moving parts of a struct, but can’t express in the type system a struct type that has some members inaccessible due to being borrowed or moved out.

This means, for example, that if you keep a borrow &self.foo, you can no longer call any method on &mut self, even if that method only really needs to modify self.bar and doesn’t touch self.foo. To work around this, you are forced to replace the &mut self with individual &muts to fields, which can be unergonomic.

So a possible idea could be to introduce “partial struct types”, which would be denoted as StructName {field1, field2 {subfield1, subfield2, …}, …, fieldn, &borrowed1, …, &borrowedn}. This type would be exactly like StructName, except that only the named fields (and recursively subfields if specified) can be accessed, and the ones with the “&” only support taking an & but not an &mut. Perhaps a !field1, !field2 syntax could be introduced to denote “all fields except field1 ad field2” (also supporting the &).

Types of struct variables would automatically “morph” into partial struct types as borrows happen. Essentially, instead of keeping that state in the borrow checker internally, it would now be made explicit in the type of the variable itself.

For private methods and public methods called from the same crate, the types of parameters would be automatically replaced with the most restrictive partial struct type, and you would thus be able to call methods that would trigger the borrow checker otherwise.

Downsides and possible issues:

  • Adds a complex subtyping hierarchy, not sure of the impact on type inference, etc.
  • Requires (per-crate) global type inference, since typing out the partial types would be too annoying
  • Might be challenging to implement while keeping compilation times low
  • Inference on private calls can result in “action at a distance” where modifying some code results in borrow checker errors far away
  • Authors of libraries with structs that have public fields (or that take parameters that are &/&mut of a struct with public fields) would have to explicitly decide whether to use partial types for each of the methods.

Extensions:

  • Could do it with enums too, after having implemented first-class types for variants

#2

I would like to address this problem, but I am quite wary of making the language / type-system feel far more complicated. I think there are many facets to be weighed here. Among them, I think we can separate out the “public interface” of a type from its implementation.

What I’ve found is that virtually all the time that I get annoyed by this, it’s because I am implementing some function, and I have some (typically private) helper routines that borrow from a field, and I want to invoke some other helper routine that will mutate some (other) field. Example:

struct Foo { a, b }

impl Foo {
    fn a_elements(&self) -> &[A] { &self.a }
    fn throb_b(&mut self) { self.b.throb() }

    pub fn do_something() {
        for x in self.a_elements() {
           self.throb_b();
        }
    }
}

Of course, this code will work if I inline the helpers, because the borrow-check works “per function body”, and it tracks extended state at that level.

I have in the past contemplating the idea of extending this idea past function boundaries, so that the borrow checker can use an extended analysis to track across private methods/functions within a given module. This would mean that the code above might work, but if we made all those functions public, then public callers would not get the benefit. One nice aspect of this is that code like the code I wrote above would just work with no additional annotations (because we are, effectively, inferring them – just as we do within a function body). I think it’s pretty vital that we keep annotation burden to a minimum.

Note that inference is only plausible within a crate. Across crates, using inference to decide which functions “compose” with one another would clearly lead to a lot of subtle semver hazards. Therefore, if we wanted to tackle public interfaces across crates, we would need some form of declarations. In that case, a system like the one you describe might be the way to go. There is lots of “prior art” here that is worth exploring (e.g., you can also use effects and regions to do this, or you can have composability declarations, and so forth).

But I really feel like almost all the time this comes up, it’s more about private implementation details. When you’re working “from the outside”, I at least tend to think of most things as atomic entities, and I don’t feel the need to invoke mutable methods on it while simultaneously manipulating it. (Also, it’s worth pointing out that the fields in traits proposal offers some ways for public APIs to expose disjointness as well.)

I’m curious though if we can make progress here by extending the current inference to go beyond function boundaries without solving the problem of exposing this in your public API. I’m not sure if this makes the borrow checker “too magical” – we might need to try some experimentation to decide.


#3

Yeah, actually only supporting the in-crate case implicitly as a “crate-global borrow checker” and not adding any syntax seems a better option, at least at first; I got a bit carried away with the idea.

I have read somewhere that the borrow checker still needs to be ported to run on MIR: if that’s indeed the case, perhaps this could either be implemented as part of that effort, or otherwise the port could be done with some attention to making sure that the architecture is such that it is possible to implement this later easily.


#4

@pnkfelix has been working on this. An initial version should be coming soon. I would indeed prefer to build on that.