functional updates
I’m a big fan of functional programming, so naturally I want to handle state updates functionally. Coming from high-level languages, like Scala, where immutability was abstracted away, I thought it was enough to write functions that return new data rather than mutating existing data. But working in Rust forced me to understand the underlying mechanics: the distinction between consuming ownership and returning new values.
Consuming vs Borrowing when Updating
-
Take ownership (or consume) with
self
Take ownership, bind as mutable. -
Borrow: I’m forced to clone!
Since I only have&self(a reference), I can’t move the data out. To create a newSomeData, I must clone it!
impl SomeData {
// Takes ownership - consumes the board!
pub fn update_1(self, x: T) -> SomeData {
let mut data = self.data; // Just move, no clone!
data.insert(x);
SomeData { data }
}
// Borrows - board can still be used after
pub fn update_2(&self, x: T) -> SomeData {
let mut new_data = self.clone(); // Must clone!
new_data.data.insert(x);
new_data
}
}
What happens with self:
let some_data = SomeData::new(...);
let new_data = some_data.update_1(x); // some_data moved
let newer_data = new_data.update_1(y); // some_data is NOT available!
This is the builder pattern or functional update pattern — returning a new/modified board each time.
What happens with &self:
let new_data = some_data.update_2(x); // some_data cloned
let newer_data = some_data.update_2(y); // some_data is still available!
Mutable/imperative style (modify in place)
What happens with &mut self:
impl SomeData {
pub fn update(&mut self, x: T) {
self.data.insert(x) // modify self directly
}
}
// Usage:
let mut some_data = SomeData::new(...);
some_data.update(x);
some_data.update(x);
Rule of Thumb
- use
selffor transforming and updating - use
&selffor reading data (query/getter) - use
&mut selfto modify in place, if that’s what you are into