Search code examples
referencerustmutable

How can I recache data whenever a mutable reference to the source data is dropped?


I have an struct called Spire that contains some data (elements), and a cache of some result that can be calculated from that data. When elements changes, I want to be able to automatically update the cache (e.g. without the user of the struct having to manually call update_height in this case).

I'm trying to figure out how I can achieve that, or if there is a better way to do what I'm trying to do.

struct Spire {
    elements: Vec<i32>,
    height: i32,
}

impl Spire {
    pub fn new(elements: Vec<i32>) -> Spire {
        let mut out = Spire {
            elements: elements,
            height: 0,
        };
        out.update_height();
        out
    }

    pub fn get_elems_mut(&mut self) -> &mut Vec<i32> {
        &mut self.elements
    }

    pub fn update_height(&mut self) {
        self.height = self.elements.iter().sum();
    }

    pub fn height(&self) -> i32 {
        self.height
    }
}

fn main() {
    let mut spire = Spire::new(vec![1, 2, 3, 1]);

    // Get a mutable reference to the internal elements
    let spire_elems = spire.get_elems_mut();

    // Do some stuff with the elements
    spire_elems.pop();
    spire_elems.push(7);
    spire_elems.push(10);

    // The compiler won't allow you to get height
    // without dropping the mutable reference first
    // dbg!(spire.height());

    // When finished, drop the reference to the elements.
    drop(spire_elems);
    // I want to automatically run update_height() here somehow

    dbg!(spire.height());
}

Playground

I am trying to find something with behavior like the Drop trait for mutable references.


Solution

  • There are at least two ways to tackle this problem. Instead of calling drop directly, you should put your code which does the mutation in a new scope so that the scoping rules will automatically be applied to them and drop will be called automatically for you:

    fn main() {
        let mut spire = Spire::new(vec![1, 2, 3, 1]);
    
        {
            let spire_elems = spire.get_elems_mut();
            spire_elems.pop();
            spire_elems.push(7);
            spire_elems.push(10);
        }
    
        spire.update_height();
        dbg!(spire.height());
    }
    

    If you compile this, it will work as expected. Generally speaking, if you have to call drop manually it usually means you are doing something that you shouldn't do.

    That being said, the more interesting question is designing an API which is not leaking your abstraction. For example, you could protect your internal data structure representation by providing methods to manipulate it (which has several advantages, one of them is that you can freely change your mind later on what data structure you are using internally without effecting other parts of your code), e.g.

    impl Spire {
        pub fn push(&mut self, elem: i32) {
            self.elements.push(elem);
            self.update_internals();
        }
    }
    

    This example invokes a private method called update_internals which takes care of your internal data consistency after each update.

    If you only want to update the internal values when all the additions and removals have happened, then you should implement a finalising method which you have to call every time you finished modifying your Spire instance, e.g.

    spire.pop();
    spire.push(7);
    spire.push(10);
    spire.commit();
    

    To achieve such a thing, you have at least another two options: you could do it like the above example or you could use a builder pattern where you are doing modifications throughout a series of calls which will then only have effect when you call the last finalising call on the chain. Something like:

    spire.remove_last().add(7).add(10).finalise();
    

    Another approach could be to have an internal flag (a simple bool would do) which is changed to true every time there is an insertion or deletion. Your height method could cache the calculated data internally (e.g. using some Cell type for interior mutability) and if the flag is true then it will recalculate the value and set the flag back to false. It will return the cached value on every subsequent call until you do another modification. Here's a possible implementation:

    use std::cell::Cell;
    
    struct Spire {
        elements: Vec<i32>,
        height: Cell<i32>,
        updated: Cell<bool>,
    }
    
    impl Spire {
        fn calc_height(elements: &[i32]) -> i32 {
            elements.iter().sum()
        }
    
        pub fn new(elements: Vec<i32>) -> Self {
            Self {
                height: Cell::new(Self::calc_height(&elements)),
                elements,
                updated: Cell::new(false),
            }
        }
    
        pub fn push(&mut self, elem: i32) {
            self.updated.set(true);
            self.elements.push(elem);
        }
    
        pub fn pop(&mut self) -> Option<i32> {
            self.updated.set(true);
            self.elements.pop()
        }
    
        pub fn height(&self) -> i32 {
            if self.updated.get() {
                self.height.set(Self::calc_height(&self.elements));
                self.updated.set(false);
            }
            self.height.get()
        }
    }
    
    fn main() {
        let mut spire = Spire::new(vec![1, 2, 3, 1]);
        spire.pop();
        spire.push(7);
        spire.push(10);
        dbg!(spire.height());
    }
    

    If you don't mind borrowing self mutably in the height getter, then don't bother with the Cell, just update the values directly.