Lookup or create value and return by reference


#1

I’m trying to implement the following:

  1. I have a DocumentCollection.
  2. I want open_document to return an existing document if present.
  3. If not present then I want it to create and return a new document.
  4. I’d like to keep the document lookup by name logic in a separate function.

I “think” I understand the reported error. I guess as soon as return is seen then that value needs to be borrowed for the entire function… because the checker can’t determine when that return might get called (might be loops, conditions, etc). And so that’s borrowing self for the entire function and that means I can’t do the insert part.

So I think I see why rust is complaining, but I’m drawing a blank on how to actually solve the problem. Feeling a bit dumb, suggestions appreciated!

struct Document {
  name: String,
}

struct DocumentCollection {
  documents: Vec<Document>,
}

impl DocumentCollection {
  fn open_document(&mut self, name: String) -> Option<&Document> {
    if let Some(document) = self.lookup_document(&name) {
      return Some(document);
    }
    let document = Document { name };
    self.documents.push(document);
    self.documents.last()
  }

  fn lookup_document(&self, name: &str) -> Option<&Document> {
    for each in &self.documents {
      if each.name == name {
        return Some(each);
      }
    }
    None
  }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `self.documents` as mutable because it is also borrowed as immutable
  --> src/lib.rs:15:5
   |
10 |   fn open_document(&mut self, name: String) -> Option<&Document> {
   |                    - let's call the lifetime of this reference `'1`
11 |     if let Some(document) = self.lookup_document(&name) {
   |                             ---- immutable borrow occurs here
12 |       return Some(document);
   |              -------------- returning this value requires that `*self` is borrowed for `'1`
...
15 |     self.documents.push(document);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.


#2

This particular issue should be solved with Polonius, whenever that gets released.

In the meantime, the easiest workaround is to find the index of the document, if it exists, and then either return a reference at that index or proceed to insert the document. Something like:

fn open_document(&mut self, name: String) -> Option<&Document> {
        let pos = self.documents.iter().position(|d| d.name == name);
        if let Some(pos) = pos {
            Some(&self.documents[pos])
        } else {
            let document = Document { name };
            self.documents.push(document);
            self.documents.last()
        }
    }

#3

Thanks, I don’t feel quite so dumb now!


#4

Yeah, it’s not dumb at all. I failed to link to @nikomatsakis’s blog post that covers this precise case as sort of the poster child for polonius: http://smallcultfollowing.com/babysteps/blog/2018/06/15/mir-based-borrow-check-nll-status-update/