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:
- Obtain a mutable owned
Book
. - Use its methods to mutate it.
- Use its methods to obtain references from it.
- 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.