How to partially borrow from struct?

Here is a tiny reproducible replica of my code.

use std::collections::{HashMap};

struct Foobar {
    foo: HashMap<char, i32>,
    bar: Vec<i32>,
}

impl Foobar {
    fn new() -> Self {
        Self {
            foo: HashMap::new(),
            bar: Vec::new(),
        }
    }

    fn get_item_from_foo(&mut self, t: char) -> &mut i32 {
        self.foo.entry(t).or_insert(0)
    }

    fn push_to_bar(&mut self, t: char) {
        let borrow = self.get_item_from_foo(t);
        self.bar.push(0);
        *borrow = 0;
    }
}

You can also see the code here at the playground.

Let me explain: in the function push_to_bar I firstly borrow an item form self.foo, then append an element to self.bar, at last I need to modify the borrowed item. This seems correct. But the compiler gives me an error.

error[E0499]: cannot borrow `self.bar` as mutable more than once at a time
  --> src/main.rs:22:9
   |
21 |         let borrow = self.get_item_from_foo(t);
   |                      ---- first mutable borrow occurs here
22 |         self.bar.push(0);
   |         ^^^^^^^^ second mutable borrow occurs here
23 |         *borrow = 0;
   |         ----------- first borrow later used here

error: aborting due to previous error

In my comprehension, the function get_item_from_foo doesn't borrow the entire self. And there is no shared memory between foo and bar. What's the wrong with my code? I'm glad if anyone can help.

Sadly Rust does not see it like that: Rust inspects borrowing at the API / prototype / function signature level.

And the signature of get_item_from_foo borrows self: &mut Foobar.

The reason Rust works like that is that it favors local reasoning w.r.t. code changes / refactoring: imagine later someone changes get_item_from_foo to instead use a borrow from self.bar: this would not contradict the function's signature / API, but it would make the push_to_bar function unsound. So it is way simpler to just reason in terms of functions signatures.

The solution

Make a (private helper) function that only borrows a HashMap, then call it on &mut self.foo:

pub
struct Foobar {
    foo: HashMap<char, i32>,
    bar: Vec<i32>,
}

impl Foobar {
    pub
    fn new () -> Self
    {
        Self {
            foo: HashMap::new(),
            bar: Vec::new(),
        }
    }

    // private (helper) function
    fn get_item_from_map (
        foo: &'_ mut HashMap<char, i32>,
        t: char,
    ) -> &'_ mut i32
    {
        foo.entry(t).or_insert(0)
    }

    pub
    fn get_item_from_foo (&'_ mut self, t: char) -> &'_ mut i32
    {
        Self::get_item_from_map(&mut self.foo, t)
    }

    pub
    fn push_to_bar (&mut self, t: char)
    {
        let borrow = Self::get_item_from_map(&mut self.foo, t);
        self.bar.push(0);
        *borrow = 0;
    }
}
4 Likes

Thanks for the extensive explanation. I thought a lot and summarized my comprehension. So is my following statement correct?

For any non-closure function (i.e. declared by fn rather than pipelines |...|...), we can regard the arguments as several sources that we can borrow from, and the function can only return borrowed value that

  1. we can infer which argument is the source of borrowing;
  2. has static lifetime, or lifetime that is shorter than or equal to the source.

And after the function is called, the argument that inferred as the source of borrowing (as in 1) are considered as borrowed until the returned value is dead.

I'd appreciate if you could explain more.

The idea is that if you have

struct Foo {
    name: String,
    surname: String,
}

fn get_name<'borrow> (
    foo: &'borrow mut Foo,
) -> &'borrow String
{
    /* return */ &mut foo.name
}

then you can do:

let mut foo = Foo { ... };
let at_name = &mut foo.name;
/* `foo.name` is &mut borrowed,
   but `foo.surname` is not */
println!("{}", foo.surname);
at_name.clear();

but you cannot do:

let mut foo = Foo { ... };
let at_name = get_name(&mut foo);
/* the whole `foo` is now &mut borrowed,
   including `foo.surname` */
println!("{}", foo.surname); // Error
at_name.clear();

indeed, now at_name is a borrow that goes through the get_name function abstraction. And what is the contract of this abstraction?

fn get_name<'borrow> (
    foo: &'borrow mut Foo,
) -> &'borrow String;

That is, the function returns a mut borrow to a String that lives at least as much as lives the input mut borrow on Foo.
"We" / Rust know(s) nothing about the kind of access that get_name does. Maybe it borrows the .name field, maybe it borrows foo.surname, or maybe even it does not borrow any of those, and returns something else (by leaking memory).

So, for usages of get_name() calls to be sound, Rust must assume the worst: that all the fields of Foo are being borrowed. It is indeed a conservative and hindering assumption, but at least it does mange to prevent memory unsafety.

That's why one must pay attention to the signatures / abstractions of a function, since by requiring it to take a full Foo instead of a more specific field (like the HashMap in your example), the abstraction is having a real ergonomic impact.

3 Likes

Thank you so much! I think I've grab the concept.

Just a small addition to what has already been said: At the moment, closures are not special in this regard. Take this code for example:

struct Foo { a: i32, b: i32 }

impl Foo {
    fn broken(&mut self) {
        let mut closure = || self.b += 1;
        self.a += 1;
        closure();
    }
}

The closure borrows not just self.b mutably, but indeed the entirety of self. As a consequence, the compiler rejects this code because self.a += 1 attempts to modify self while it is already borrowed.

error[E0503]: cannot use `self.a` because it was mutably borrowed
 --> src/lib.rs:6:9
  |
5 |         let mut closure = || self.b += 1;;
  |                           -- ---- borrow occurs due to use of `self` in closure
  |                           |
  |                           borrow of `self` occurs here
6 |         self.a += 1;
  |         ^^^^^^^^^^^ use of borrowed `self`
7 |         closure();
  |         ------- borrow later used here

error: aborting due to previous error

There are plans to change this, but they are all at a very early stage for now.

1 Like