Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Destructuring a struct containing a borrow in a function argument

I am trying to implement a system that would use borrow checking/lifetimes in order to provide safe custom indices on a collection. Consider the following code:

struct Graph(i32);

struct Edge<'a>(&'a Graph, i32);

impl Graph {
    pub fn get_edge(&self) -> Edge {
        Edge(&self, 0)
    }

    pub fn split(&mut self, Edge(_, edge_id): Edge) {
        self.0 = self.0 + edge_id;
    }

    pub fn join(&mut self, Edge(_, edge0_id): Edge, Edge(_, edge1_id): Edge) {
        self.0 = self.0 + edge0_id + edge1_id;
    }
}


fn main() {
    let mut graph = Graph(0);
    let edge = graph.get_edge();
    graph.split(edge)
}

References to the graph borrowed by the Edge struct should be dropped when methods such as split or join are called. This would fulfill the API invariant that all edge indices must be destroyed when the graph is mutated. However, the compiler doesn't get it. It fails with messages like

error[E0502]: cannot borrow `graph` as mutable because it is also borrowed as immutable
  --> src/main.rs:23:5
   |
22 |     let edge = graph.get_edge();
   |                ----- immutable borrow occurs here
23 |     graph.split(edge)
   |     ^^^^^ mutable borrow occurs here
24 | }
   | - immutable borrow ends here

If I understand this correctly, the compiler fails to realise that the borrowing of the graph that happened in the edge struct is actually being released when the function is called. Is there a way to teach the compiler what I am trying to do here?

Bonus question: is there a way to do exactly the same but without actually borrowing the graph in the Edge struct? The edge struct is only used as a temporary for the purpose of traversal and will never be part of an external object state (I have 'weak' versions of the edge for that).

Addendum: After some digging around, it seems to be really far from trivial. First of all, Edge(_, edge_id) does not actually destructure the Edge, because _ does not get bound at all (yes, i32 is Copy which makes things even more complicated, but this is easily remedied by wrapping it into a non-Copy struct). Second, even if I completely destructure Edge (i.e. by doing it in a separate scope), the reference to the graph is still there, even though it should have been moved (this must be a bug). It only works if I perform the destructuring in a separate function. Now, I have an idea how to circumvent it (by having a separate object that describes a state change and destructures the indices as they are supplied), but this becomes very awkward very quickly.

like image 289
MrMobster Avatar asked Jan 26 '17 08:01

MrMobster


1 Answers

You have a second problem that you didn’t mention: how does split know that the user didn’t pass an Edge from a different Graph? Fortunately, it’s possible to solve both problems with higher-rank trait bounds!

First, let’s have Edge carry a PhantomData marker instead of a real reference to the graph:

pub struct Edge<'a>(PhantomData<&'a mut &'a ()>, i32);

Second, let’s move all the Graph operations into a new GraphView object that gets consumed by operations that should invalidate the identifiers:

pub struct GraphView<'a> {
    graph: &'a mut Graph,
    marker: PhantomData<&'a mut &'a ()>,
}

impl<'a> GraphView<'a> {
    pub fn get_edge(&self) -> Edge<'a> {
        Edge(PhantomData, 0)
    }

    pub fn split(self, Edge(_, edge_id): Edge) {
        self.graph.0 = self.graph.0 + edge_id;
    }

    pub fn join(self, Edge(_, edge0_id): Edge, Edge(_, edge1_id): Edge) {
        self.graph.0 = self.graph.0 + edge0_id + edge1_id;
    }
}

Now all we have to do is guard the construction of GraphView objects such that there’s never more than one with a given lifetime parameter 'a.

We can do this by (1) forcing GraphView<'a> to be invariant over 'a with a PhantomData member as above, and (2) only ever providing a constructed GraphView to a closure with a higher-rank trait bound that creates a fresh lifetime each time:

impl Graph {
    pub fn with_view<Ret>(&mut self, f: impl for<'a> FnOnce(GraphView<'a>) -> Ret) -> Ret {
        f(GraphView {
            graph: self,
            marker: PhantomData,
        })
    }
}

fn main() {
    let mut graph = Graph(0);
    graph.with_view(|view| {
        let edge = view.get_edge();
        view.split(edge);
    });
}

Full demo on Rust Playground.

This isn’t totally ideal, since the caller may have to go through contortions to put all its operations inside the closure. But I think it’s the best we can do in the current Rust language, and it does allow us to enforce a huge class of compile-time guarantees that almost no other language can express at all. I’d love to see more ergonomic support for this pattern added to the language somehow—perhaps a way to create a fresh lifetime via a return value rather than a closure parameter (pub fn view(&mut self) -> exists<'a> GraphView<'a>)?

like image 54
Anders Kaseorg Avatar answered Oct 15 '22 09:10

Anders Kaseorg