Grouped borrow checking in calls?

I recently finished The Book and have been working on my first open source crate — a bookkeeping library for everyone — for a while now, when I came across this.

My intention behind this API design was to maximize usage of the borrow checker for the benefit of the user. Specifically:

  1. Obtain a mutable owned Book.
  2. Use its methods to mutate it.
  3. Use its methods to obtain references from it.
  4. Use these references as the definition for further mutation of the same instance of the Book.

One of the benefits in this theoretical approach is that any obtained references would be usable only until the next mutation. This is supposed to decrease likelihood of usage of outdated references / calculations derived from the Book.

The other theoretical benefit is that the calls to mutate that require some kind of reference to existing internal state would be safer and have less ways of failing. Specifically, the call to create a move between to accounts would be defined by two references to accounts rather than some identifying values that may or may not actually be valid for the current state of the Book.

Yet, this does not seem possible, because the method Book::new_move borrows the Book instance mutably, invalidating the references to the accounts before they could be passed in as arguments to that method.

/// Simplified bookkeeping crate
pub mod bookkeeping {
    pub struct Book(Vec<Account>);
    pub struct Account {
        name: String,
        pub(crate) balance: i8,
    }
    impl Book {
        pub fn new() -> Self {
            Book(Vec::new())
        }
        pub fn new_account(&mut self, name: String) {
            self.0.push(Account { name, balance: 0 });
        }
        pub fn accounts(&self) -> &[Account] {
            &self.0[..]
        }
        pub fn new_move(&mut self, from: &Account, to: &Account, amount: i8) {
            self.0
                .iter_mut()
                .find(|account| account.name == from.name)
                .unwrap()
                .balance -= amount;
            self.0
                .iter_mut()
                .find(|account| account.name == to.name)
                .unwrap()
                .balance -= amount;
        }
    }
}
fn main() {
    use bookkeeping::Book;
    // The book is mutable, so the references obtain from it are
    // valid only until the next mutation. That's the benefit I sought to have.
    let mut book = Book::new();
    // mutate
    book.new_account(String::from("wallet"));
    book.new_account(String::from("bank"));
    // Obtain references that will be used in specifying the next mutation.
    let mut accounts = book.accounts().iter();
    let wallet = accounts.next().unwrap();
    let bank = accounts.next().unwrap();
    // It seems that borrow rules do not allow this, because as soon as the book
    // is mutably borrowed by the method, all current references are invalid.
    book.new_move(wallet, bank, 500);
    // Obtain new references.
    let mut accounts = book.accounts().iter();
    let wallet = accounts.next().unwrap();
    let bank = accounts.next().unwrap();
    assert_eq!(wallet.balance, -500);
    assert_eq!(bank.balance, 500);
}

Is this API logically sound?

I will appreciate any and all illumination.

I have a real working implementation of this library working around this issue via generous usage of Rc, here.

In this case I'd use shared mutability:

use ::core::cell::Cell as Mut;
    pub struct Account {
        name: String,
-       pub(crate) balance: i8,
+       pub(crate) balance: Mut<i8>,
    }
    ...

-       pub fn new_move(&mut self, from: &Account, to: &Account, amount: i8) {
+       pub fn new_move(&    self, from: &Account, to: &Account, amount: i8) {
            self.0
                .iter()
                .find(|account| account.name == from.name)
                .unwrap()
-               .balance -= amount;
+               .balance.update(|balance| balance - amount);
            self.0
                .iter()
                .find(|account| account.name == to.name)
                .unwrap()
-               .balance += amount;
+               .balance.update(|balance| balance + amount);
        }
    }
  • with #![feature(cell_update)]

    or with the following stable polyfill
    trait CellUpdate {
        type T;
        fn update (&self, map: impl FnOnce(Self::T) -> Self::T);
    }
    impl<T : Copy> CellUpdate for ::core::cell::Cell<T> {
        type T = T;
        fn update (&self, f: impl FnOnce(T) -> T)
        {
            self.set(f(self.get())
        }
    }
    

That way, you only invalidate the Accounts when new accounts are created, but not when simply modifying their balance.

1 Like

Thank you.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.