This is great, I think I've got my head around the general problem much more fully thanks to all your help, @dodomorandi.
I worked through your code and here's how it works out in terms of ergonomics:
let mut mx = sync::Mutex::new(vec![5, 6]);
let mg = MutexGuard::from(&mut mx).unwrap();
let mut iter = mg.iter();
while let Some(x) = iter.next() {
println!("{}", x);
}
I don't think any of that can be simplified since you need the mx
binding to live long enough for the call to MutexGuard::from
and you need the mg
binding to live long enough for the call to MutexGuard::iter
.
To better understand why this approach worked, I tried writing some functions to see if I could eliminate the need for the MutexGuard
struct, since it just wraps std::sync::MutexGuard
. My first try was:
fn mutex_iter<'g, 'm: 'g, T: 'm>(mutex: &'m mut sync::Mutex<T>) -> MutexGuardIter<'g, T>
where
&'g T: IntoIterator,
{
let iter = mutex.lock().unwrap().deref().into_iter();
MutexGuardIter { iter, elem: None }
}
But that fails for basically the same reason you can't eliminate the mg
binding above:
error[E0716]: temporary value dropped while borrowed
|
25 | fn mutex_iter<'g, 'm: 'g, T: 'm>(mutex: &'m mut sync::Mutex<T>) -> MutexGuardIter<'g, T>
| -- lifetime `'g` defined here
...
29 | let iter = mutex.lock().unwrap().deref().into_iter();
| ^^^^^^^^^^^^^^^^^^^^^--------------------- temporary value is freed at the end of this statement
| |
| creates a temporary which is freed while still in use
| argument requires that borrow lasts for `'g`
So, instead I tried this:
fn mutex_guard_iter<'g, 'm: 'g, T: 'm>(
mutex_guard: &'g sync::MutexGuard<'m, T>,
) -> MutexGuardIter<'g, T>
where
&'g T: IntoIterator,
{
MutexGuardIter { iter: mutex_guard.deref().into_iter(), elem: None }
}
Which does eliminate the need for the extra wrapper type, but the usage ergonomics are basically the same:
let mut mx = sync::Mutex::new(vec![5, 6]);
let mg = mx.lock().unwrap();
let mut iter = mutex_guard_iter(&mg);
while let Some(x) = iter.next() {
println!("{}", x);
}
At this point it occurred to me: all the code we've worked through sill relies on the same constraint: the guard, the (potentially streaming) iterator and the current value all need to have lifetimes dependent upon one another, but due to the way lifetimes work, we can't package up all that complexity behind a nice interface while still allowing for the nice ergonomics we want.
If we're willing to just return the std::sync::MutexGuard
guard to the consumer:
fn iterable_mutex_guard<T>(mutex: &mut sync::Mutex<T>) -> sync::MutexGuard<T>
where
T: IntoIterator,
{
mutex.lock().unwrap()
}
the ergonomics are great:
let mut mx = sync::Mutex::new(vec![5, 6]);
for x in iterable_mutex_guard(&mut mx).iter() {
println!("{}", x);
}
But we just improved ergonomics at the expense of worse encapsulation. Now we can also do this:
let mut mx = sync::Mutex::new(vec![5, 6]);
let mut mg = iterable_mutex_guard(&mut mx);
*mg = vec![7, 8];
for x in mg.iter() {
println!("{}", x);
}
At that point, we might as well just be returning the &mut std::sync::Mutex
itself.
Perhaps the thing that gives best balance between ergonomics and encapsulation would be a wrapper around the guard that gives easy access to the Iterator
interface, but without totally exposing the value the guard owns:
pub struct IterableGuard<'m, T>(sync::MutexGuard<'m, T>);
impl<'m, I> IterableGuard<'m, Vec<I>> {
pub fn from(mutex: &'m mut sync::Mutex<Vec<I>>) -> Self {
mutex.lock().map(Self).unwrap()
}
pub fn iter(&self) -> impl Iterator<Item = &I> {
self.0.iter()
}
}
I implement this specifically for Vec
because I need to call slice::iter
, which can return an owned slice::Iter
from a shared reference to self
rather than IntoIterator
that takes ownership of self
. (There may be a way to do it with using a IntoIterator
bound on &T
rather than T
, but I got into the conflicting lifetimes trap again when I tried.)
This results in nice ergonomics:
let mut mx = sync::Mutex::new(vec![5, 6]);
let ig = IterableGuard::from(&mut mx);
for x in ig.iter() {
println!("{}", x);
}
But it doesn't expose the guarded value, since IterableGuard
doesn't implement Deref
. And it can be expanded to support container types other than Vec
with a pretty modest amount of code.
It's even possible to make things a bit more generic and generalize separately over the guard and collection type:
pub struct IterableGuard<T, G: Deref<Target = T>>(G);
impl<'m, T> IterableGuard<T, sync::MutexGuard<'m, T>> {
pub fn from_mutex(mutex: &'m mut sync::Mutex<T>) -> Self {
mutex.lock().map(Self).unwrap()
}
}
impl<'a, T> IterableGuard<T, sync::RwLockReadGuard<'a, T>> {
pub fn from_read_lock(mutex: &'a mut sync::RwLock<T>) -> Self {
mutex.read().map(Self).unwrap()
}
}
impl<I, G: Deref<Target = Vec<I>>> IterableGuard<Vec<I>, G> {
pub fn iter(&self) -> impl Iterator<Item = &I> {
self.0.iter()
}
}
impl<K, V, G: Deref<Target = HashMap<K, V>>> IterableGuard<HashMap<K, V>, G>
where
K: std::cmp::Eq + std::hash::Hash,
{
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.0.iter()
}
}
Though I wonder if that's sacrificing too much in terms of readability.