Partially borrowed/moved struct types

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 Likes