A way to coerce away / re-borrow mut inside a type?

Hello,

I have accepted that it's usually necessary for me to write separate mut and "const" code paths. If the behavior is very simple I can sometimes unify the logic with a generic type parameter and then just instantiate two different versions of the method. But often that comes out being less maintainable than two versions when the lifetimes are complicated.

But I want to know if I can avoid a third version of the code, when data types have nested mut qualifiers inside the Types.

Specifically, if I want to call onto the const code path from the mut code path, is it possible to coerce the Type to get rid of the internal mut? I figured it might be possible because a single reference can be re-borrowed as const, so why not a whole structure?

Here is a toy example using the const code path to preflight the mut code path.

fn get_from_slice<'a>(slice: &'a[&'a usize]) -> Option<&'a usize> {
    slice.get(0).map(|v| &**v)
}

fn get_from_mut_slice<'a>(slice: &'a mut[&'a mut usize]) -> Option<&'a mut usize> {
    if get_from_slice(&*slice).is_none() {
        return None
    }

    slice.get_mut(0).map(|v| &mut **v)
}

fn main() {
    let mut one: usize = 1;
    let mut two: usize = 2;
    let mut slice = [&mut one, &mut two];
    let result = get_from_mut_slice(&mut slice);
    println!("{:?}", result);
}

Thank you.

Well, from the reference we get the coersion rules. The one that fits best is this one:

  • &T or &mut T to &U if T implements Deref<Target = U>

In this case T would be [&mut usize] and U would be [&usize]. But there is no deref coercion between these. So my best guess is that it's not possible.

Then there's also this case

  • T to U if T is a subtype of U (reflexive case)

But T=&mut S is invariant in S so it cannot be a subtype of U=&V. ( S = &mut usize and V=&usize)

Lastly, there is the rule

  • &mut T to &T
    But oviously the inner type T is not the same for both.
1 Like

In general, no.

But as you said, often generics can be used.

fn get_from_slice<'a, T>(slice: &'a[T]) -> Option<&'a T> {
    slice.get(0)
}

fn get_from_mut_slice<'a, T>(slice: &'a mut [T]) -> Option<&'a T> {
    if get_from_slice(&*slice).is_none() {
        return None
    }

    slice.get_mut(0).map(|v| &*v)
}

That's not the signature you want, by the way.

1 Like

That's not the signature you want, by the way

Yes. Sorry I should have been more clear. This below is the signature I want.

fn get_from_slice<'a, T: core::borrow::Borrow<usize> + 'a>(slice: &[T]) -> Option<&'a usize> {
    slice.get(0).map(|v| v.borrow())
}

But I couldn't figure out how to bound the borrowed value, so instead I went with this, which is definitely inferior:

fn get_from_slice<'a, T: core::borrow::Borrow<usize>>(slice: &'a[T]) -> Option<&'a usize> {
    slice.get(0).map(|v| v.borrow())
}

The same-lifetime-on-reference-and-referant was just a side-effect of me expanding this generic in the post.

Thanks for that excellent writeup of this anti-pattern.

If that could work,[1] you could implement this:

fn extend_lifetime_of_borrow(u: &usize) -> &'static usize {
    get_from_slice(std::slice::from_ref(u)).unwrap()
}

I.e. it would be unsound. You only have access to the items in the slice for the lifetime on the &[T], so the signature you went with is the proper one. Since there's only one lifetime in the signature now, you can elide it:

fn get_from_slice<T: core::borrow::Borrow<usize>>(slice: &[T]) -> Option<&usize> {

  1. without always returning None or a reference to something outside the slice, like some static or leaked value ↩ī¸Ž

2 Likes

I wish there were something like the Borrow trait with an explicit lifetime parameter.

trait BoundedBorrow<'a, T> {
    fn bounded_borrow<'s, 'out>(&'s self) -> &'out T where 'a: 'out, 's: 'out;
}

impl<'a> BoundedBorrow<'a, usize> for &'a mut usize {
    fn bounded_borrow<'s, 'out>(&'s self) -> &'out usize where 'a: 'out, 's: 'out {
        &**self
    }
}

fn get_from_slice<'a, T: BoundedBorrow<'a, usize>>(slice: &[T]) -> Option<&'a usize> {
    slice.get(0).map(|v| v.bounded_borrow())
}

:frowning:

When I look at that how it's written, 'a doesn't really do anything in BoundedBorrow other than act as an upper limit for 'out. And in your only implementation, 's is also an upper limit on 'a. So you could just get rid of 'a and it wouldn't act any different for that implementation...

trait BoundedBorrow<T> {
    fn bounded_borrow<'s, 'out>(&'s self) -> &'out T where 's: 'out;
}

...and all 'out is doing here is letting you return a shorter lifetime than 's, but you still have to borrow self for 's to call the method, and the lifetime on the returned reference can be shrunk by covariance if it really needs to, so you can get rid of that too...

trait BoundedBorrow<T> {
    fn bounded_borrow<'s>(&'s self) -> &'s T 
}

...and now we see it's no different than Borrow<T> really, which is why it still gave the error with your get_from_slice function.


Maybe you meant something like you wish you could get a &'a T from a &'short &'a mut T, since that would make get_from_slice compile...

trait BoundedBorrow<'a, T> {
    fn bounded_borrow(&self) -> &'a T;
}

impl<'a> BoundedBorrow<'a, usize> for &'a mut usize {
    fn bounded_borrow(&self) -> &'a usize {
        todo!()
    }
}

...but you can't soundly do that, because once the outer &'short of the &'short &'a mut T "expires", the &'a mut T must once again be exclusive. Example.


Maybe some sort of shared mutation like a RefCell would help here, or maybe there's some other design change at a higher level.

1 Like