Can one get rid of _mut?

It's mildly annoying having to write everything twice, for _mut and non-_mut.

Would something like this be possible?

fn head<'a, S: MaybeMut<'a, Self>>(self: S) -> S::MaybeMut<'a, T>

(or this

fn head<'a, mut r>(&'a r self) -> &'a r T {
  &r self.value
}

but this would probably require changes to the language. altho so would the former, probably. meh.)

No. If there was an easy way to do this, the standard library would be doing that.

5 Likes

well... we don't have GATs yet, so ofc.

Explicitness is really important when reading (rust) code, and especially so when you need to know if you have an exclusive (not mutable) reference to T. The way the language is now is pretty solid

5 Likes

GATs don't wholly solve the problem because &T and &mut T are fundamentally quite different, and the compiler has to use different reasoning with them. mut is more than just a modifier on top of &. That doesn't mean the idea is without merit. There are a number of earlier discussions on this topic; searching for abstract over mutability has some promising results. Also check IRLO (that is, internals.rust-lang.org); it's been floated over there as well.

7 Likes

You can kinda make it work with GATs if you use unsafe/raw pointers in the implementation.

You lose borrowck but that's "okay".

It's unclear how exactly you suggest using raw pointers, but I suspect it would violate the rule that casting an &T to an &mut T is insta-UB, even if the &T was originally created from an &mut T, and even if the &mut T is not used to modify anything.

2 Likes

nah it involves having MaybeMut::from_raw and MaybeMut::into_raw.

so you'd write something like uh...

fn head<'a, S: MaybeMut<'a, Self>>(self: S) -> S::MaybeMut<'a, T> {
  unsafe { S::MaybeMut::<'a, T>::from_raw(addr_of!((*self.into_raw()).value)) }
}

we think? not entirely sure how pointer projections work.

for &mut T this goes through as *mut T as *const T and for &T this just goes to as *const T etc.

I think the simplest and almost trivial to implement solution would do to just create a macro which may turn something like 𝓢𝓾𝓽0 into either nothing or mut.

Then you can write it like this:

fn head<'a>(&'a 𝓢𝓾𝓽0 self) -> &'a 𝓢𝓾𝓽0 T {
  &𝓢𝓾𝓽0 self.value
}

And macro would expand it into two (four, eight, etc) versions. Probably Ok to support just 𝓢𝓾𝓽0 to 𝓢𝓾𝓽9 (enough to create 1024 functions which is probably already beyond what's practically usable).

At least that way compiler may verify that what you are doing makes sense for both exclusive references and non-exclusive references.

1 Like

Yes, you are using addr_of! and that is ok in this case, but people often do this wrong, going through one of the reference types. Additionally, there's no addr_of! equivalent for things like a HashMap lookup.

1 Like

Introducing mut generics wouldn't help in that case either, unless HashMap was expanded to take advantage of it...

But that seems kinda niche, and you might be able to get away with UnsafeCell in that case. (no transmute tho. do not transmute. transmute bad. love how UnsafeCell doesn't have niches so it isn't actually transparent. but uhh anyway we digress.)

possibly something like this?

#![feature(arbitrary_self_types)]
#![feature(generic_associated_types)]

use core::ops::Deref;
use core::ptr::addr_of;

unsafe trait Reference {
    type As<'a, T: 'a>: Deref<Target = T>;
    
    unsafe fn from_raw<'a, T>(raw: *mut T) -> Self::As<'a, T>;
}

struct Ref;

unsafe impl Reference for Ref {
    type As<'a, T: 'a> = &'a T;
    
    unsafe fn from_raw<'a, T>(raw: *mut T) -> Self::As<'a, T> {
        &*raw
    }
}

struct Mut;

unsafe impl Reference for Mut {
    type As<'a, T: 'a> = &'a mut T;
    
    unsafe fn from_raw<'a, T>(raw: *mut T) -> Self::As<'a, T> {
        &mut *raw
    }
}

#[derive(Default)]
struct Foo {
    a: u32,
    b: String,
}

impl Foo {
    fn num<'a, R: Reference>(self: R::As<'a, Self>) -> R::As<'a, u32> {
        unsafe { R::from_raw(addr_of!(self.a) as *mut u32) }
    }
}

fn main() {
    let mut foo = Foo::default();
    
    let _: &u32 = foo.num::<Ref>();
    let _: &mut u32 = foo.num::<Mut>();
}

which sadly causes an ice

1 Like

Hence the problem: it's not that abstracting over mutability is impossible, it's just not as useful as one might hope, because while it may be mildly helpful for trivial functions, it's no help at all for complicated ones (which are the ones you most want the compiler's help with).

It might be nice if there was a :mut fragment specifier for mutability in declarative macros, which would basically match either mut or no mut (similar to :vis). Edit: That's just $(mut)?, and is kind of a half measure itself, so ehh.

3 Likes

That's not true, there's a lot of shared codepaths for mut and non-mut cases especially for collections that make heavy use of unsafe.

The problem is that they're not currently exposed as shared.

Well... kind of? I agree there are plenty of codepaths that are syntactically similar (except for the occasional mut / _mut) but the reason they work is not necessarily the same. Consider BTreeMap:

    pub fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
    where
        K: Borrow<Q> + Ord,
        Q: Ord,
    {
        let root_node = self.root.as_ref()?.reborrow();
        match root_node.search_tree(key) {
            Found(handle) => Some(handle.into_kv().1),
            GoDown(_) => None,
        }
    }

looks quite similar to

    pub fn get_mut<Q: ?Sized>(&mut self, key: &Q) -> Option<&mut V>
    where
        K: Borrow<Q> + Ord,
        Q: Ord,
    {
        let root_node = self.root.as_mut()?.borrow_mut();
        match root_node.search_tree(key) {
            Found(handle) => Some(handle.into_val_mut()),
            GoDown(_) => None,
        }
    }

but the devil is in the details: reborrow and borrow_mut don't return the same type modulo mutability, because while reborrow can be called on any kind of reference, borrow_mut can only be called on an owned value. In fact, the node handles defined in btree_map::node have not just two or even three, but five kinds of borrowing. The only reason these methods are as short and sweet as they are is because somebody's already done the work of hiding all the unsafe behind an internal API that abstracts over borrowing type in a way that isn't generalizable, but actually is just specific to BTreeMap. Of course, this is done with a substantial amount of unsafe code, which brings me to my next point:

unsafe is exactly the code where it's most important to distinguish clearly between &T and &mut T. It's already quite easy to break aliasing rules with unsafe and not know you're doing it; writing an algorithm that has to be valid for both is even harder (using addr_of!, etc). So I think if you're mixing in unsafe, a built-in method of abstracting over mutability is probably more of a footgun than anything else. Here's a previous discussion in which I felt @Yandros made this point well:

I'm open to finding out I'm wrong here -- I think it'd be great if there's a way to add parametric mutability in a way that makes complicated stuff like BTreeMap simpler! But that's much more complex than a syntax-based transform like @VorfeedCanal's, and even the type-based solution suggested by @Mokuz doesn't really begin to address the problem of implementing a data structure with unsafe -- if anything, it might even make it worse.

By the way, I didn't pick BTreeMap deliberately because I knew it would be this complicated. I just like to remind people it exists.

4 Likes

The other alternative we can think of is associated macros. They would allow you to have:

impl<'a, T> MaybeMut<'a, T> for &'a T {
  macro r!($e:expr) { &$e }
}
impl<'a, T> MaybeMut<'a, T> for &'a mut T {
  macro r!($e:expr) { &mut $e }
}

But nobody wants this. Having mut generics enables abstractions you cannot have today. Besides,

sounds good sometimes. If it's a problem, then just don't do it.

For cases like Option::as_ref it'd be great. For cases like RefCell it'd be terrible. But this isn't an one-or-the-other situation, you can have both.

This would require expanding macros after trait resolution, which I know I'd hate.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.