Link to playground with the code presented
While programming in rust I have found myself frequently including these 2 utility functions in my code:
fn update<S>(src: &mut S, map: impl FnOnce(S) -> S) {
let value = unsafe { std::ptr::read(src) };
unsafe { std::ptr::write(src, map(value)) };
}
fn update_with<S,T>(src: &mut S, map: impl FnOnce(S) -> (T,S)) -> T {
let value = unsafe { std::ptr::read(src) };
let (result, new_value) = map(value);
unsafe { std::ptr::write(src, new_value) };
result
}
These functions let you temporarily take ownership of a mutable reference.
From the perspective of functional programming, these are very similar to the modify and state functions of the state monad, but they can also be viewed as a generalization of std::mem::replace.
As an example of why this is useful, look at the following definition of a singly linked list:
enum List<T> {
Nil,
Cons(Box<(T, List<T>)>),
}
If you wanted to implement push_front for this data structure you would have to write:
fn push_front(&mut self, x: T) {
match self {
Self::Nil => {
*self = Self::Cons(
(x, Self::Nil).into()
)
}
Self::Cons(cons) => {
let old_cons = std::mem::replace(
cons,
// temporarily have the tail
// be empty so we can aquire
// ownership of `cons` which
// will become the real tail.
(x, Self::Nil).into(),
);
// get a reference to the empty tail
let (_, xs) = cons.as_mut();
// set the actuall tail using the
// aquired `head`.
*xs = Self::Cons(old_cons);
}
}
}
This is ridiculously complicated for something that, if you had ownership of self, you could write in one line as self = Self::Cons((x, self).into()).
If we use the update function we can just write:
fn push_front(&mut self, x: T) {
update(
self,
move |slf| Self::Cons((x, slf).into())
)
}
Which is both slightly more performant (because we don't have to temporarily set the tail to be empty) and less tricky to write.
update_with is for cases where you want to update something and also return a value in the process. For example, if you wanted to implement pop_front you would have to write:
fn pop_front(&mut self) -> Option<T> {
// this time we cannot even pattern
// match so we have to set the entire
// list to be empty.
let slf = std::mem::replace(self, Self::Nil);
match slf {
Self::Nil => {
// in this case we just replaced
// nil with nil so we dont have
// to reset slf.
None
}
Self::Cons(cons) => {
let (x, xs) = *cons;
// set the actual new value of self
std::mem::replace(self, xs);
Some(x)
}
}
}
Which is, again, pretty unnecessarily complex. Though, if you tried to implement this with update you would get:
fn pop_front(&mut self) -> Option<T> {
let mut result: Option<T> = None;
update(
self,
|slf| match slf {
Self::Nil => Self::Nil,
Self::Cons(cons) => {
let (x, xs) = *cons;
result = Some(x);
xs
}
}
);
result
}
Which is still not ideal - what if the return type wasn't an Option? You would have to use unwrap.
Using update_with the function looks like:
fn pop_front(&mut self) -> Option<T> {
update_with(
self,
|slf| match slf {
Self::Nil => (None, Self::Nil),
Self::Cons(cons) => {
let (x, xs) = *cons;
(Some(x), xs)
}
}
)
}
Which is, again, way clearer.
A few questions:
- Do you know of something like this already in the standard library?
- If not, do you think this is worth adding to
std::mem? - Do you have any ideas for better names for these functions?
updateandupdate_withis just the best I could come up with, and doesn't really make it immediately obvious what they do.