Rust syntax for Cell/RefCell

Ah yeah, I see what you mean. If you can't provide that at some level it gets difficult. You could possibly create thin mut handles, that actually contain a shared reference to the real object with interior mutability member(s), but I don't see a way to avoid the extra pointer indirection, which would be bad for performance. It also seems awkward and silly to have to build a parallel shadow hierarchy of objects mirroring the real objects.

So DerefMut is defined with

    fn deref_mut(&mut self) -> &mut Self::Target;
                 ^^^^

But Cell is one of a couple types which could implement the trait requiring only &self. Under keyword generics (or just with compiler magic) the bound could default to &mut self but allow for &self.

I think we need compiler and library support for it to feel ergonomic enough tbh.

The idea of Cell is never providing a direct reference, so I do not think that is the way to go about this. Cell being able to be dereffed to a unique reference via a shared reference would lead to undefined behavior.

It could conceivably be a kind of refinement on the implementation of AddAssign for Wrapper, say.

impl<T, R> AddAssign<R> for Wrapper<T>
where
    T: Copy + AddAssign<R>,
{
    #[allow(refining_self)]
    fn add_assign(&self, rhs: R) {
        let mut inner = self.0.get();
        inner += rhs;
        self.0.set(inner);
    }
}
1 Like

I don't see why a trait that represents AddAssign for interior mutability cannot be directly implemented by Cell<T: Copy>. Could be called InteriorAddAssign

This has its own major ergonomic issues, but it is possible to implement AddAssign for shared references to a wrapped cell:

struct Counter(Cell<i32>);

impl AddAssign<i32> for &Counter {
    fn add_assign(&mut self, x: i32) {
        self.0.set(self.0.get() + x);
    }
}

fn foo(mut x: &Counter, ys: &[Counter]) {
    x += 1;
    for mut y in ys {
        y += 10;
    }
}

Unfortunately, this requires you to have a mutable place with the &Counter in it, so e.g. incrementing a field hits issues:

struct Container(Counter);
let x: &Container = ...
// These don't compile:
x.0 += 1;
&x.0 += 1;
// These do, but aren't exactly pretty:
*&mut &x.0 += 1;
{ let mut a = &x.0; a += 1; }
(&x.0).add_assign(1);

playground


That makes me think maybe the OpAssign-traits should have also taken self by value, and would typically be implemented for &mut Type (instead of Type as currently). That could have allowed implementations for &MyInteriorMutType and MyCustomMutRef as well.

1 Like

I am assuming the reason why OpAssign traits take a reference rather than a value, is to enable situations like this

let a : i32 = 4;
a += 4;

had AddAssign worked with i32 as its receiver rather than &mut i32, this won't be possible.

ive been thinking maybe +=/-= could also be used for interior mut operations. the compiler just checks if it implements InteriorAddAssign or AddAssign

There could be something like

trait InteriorAddAssign : AddAssign{}

and it would kinda work like how Fn/FnMut/FnOnce works, where doing some_fn() calls Fn, or FnMut, or FnOnce, depending on the context.

What does everyone think?

I meant

trait AddAssign<Rhs> {
    fn add_assing(self, rhs: Rhs);
}

// typical impl
impl AddAssign<i32> for &mut i32 {
    fn add_assing(self, rhs: i32) {
        *self = *self + rhs;
    }
}

That is, the method takes Self = &mut i32 by value.

However, this would also require the syntax to be able to auto-reference to allow

let mut a: i32 = 0;
a += 1;

I assume there are good reasons for the current design.


That signature is in fact much like those for the non-assign operators, so you could use + as a substitute for +=:

struct Counter(Cell<i32>);
impl std::ops::Add<i32> for &Counter {
    type Output = ();
    fn add(self, rhs: i32) {
        self.0.set(self.0.get() + rhs);
    }
}

fn main() {
    let c = Counter(Cell::new(0));
    &c + 1;
    &c + 10;
    assert_eq!(c.0.get(), 11);
}

(I don't recommend doing so for real code, though it might be useful for demonstrating the difference += on shared references could make)

Today a += b is pretty much AddAssign::add_assign(&mut a, b); If AddAssign instead took self by value and was implemented for &mut T, the compiler would need special rules for &mut T to accept T on the left-hand side.

I believe it was the right design decision for rust to make add_assign take a reference to self rather than self itself

Cell<T> dereffing to &mut T certainly would, and maybe that means the trait is inapplicable. But possibly it could deref to a place (unless I'm misusing that). In any case

// we could enable these
*cell += 4;
let k: i64 = *cell * 17;
// without enabling this
let i: &mut i64 = &*cell;

My suggestion already covers that. A type can implement a trait and they get the operator syntax. It will enable other types, not just Cell, to opt into the syntax. For example:

struct Foo{
    one: Cell<i32>,
    two: Cell<i32>
}
impl<T: Copy> InteriorAddAssign<T> for Cell<T> where T: AddAssign<T>  {
    fn interior_add_assign(&self, other: T) {
        self.update(|mut x| {x += other; x } )
    }
}
impl InteriorAddAssign for Foo {
    fn interior_add_assign(&self, other: Self) {
        self.one += ^other.one; // ^ being syntax to get T from Cell<T: Copy>. But I'm not 100% for it. but I would like something that does this though
        self.two += ^other.two;
    }
}
let foo : &Foo = &Foo::new();
let other_foo = Foo::new();

foo += other_foo; // += with Foo now valid syntax

I also proposed := syntax that can be opted in, and intend for Cell to implement it with cell.set(..), and RefCell<T> to implement it with *refcell.borrow_mut() = something

A place is not a type, so DerefMut cannot deref to a place. What generally happens is that DerefMut returns a mutable reference, and the compiler automatically adds another dereference operator to create a mutable place from that.

Getting a place for T from a &Cell<T> would be unsound anyway because you can always reborrow a place to create a new reference. You would need a new kind of place for Cells only (edit: and even then *cell += 1; would not be totally ok because it would temporarily create a mutable reference).

The language does already contain places that cannot be referenced: unaligned fields in #[repr(packed)] structs.

2 Likes

What I’ve been trying to explore is a trait+syntax approach where users implement an interior mut operator trait for their interior mut type, and then operation with that interior mut type look syntactically light. So far, nobody has really engaged with that direction directly. I haven’t seen any counter arguments/supporting arguments to that. There's mostly been suggestions that are still pretty syntactically heavy IMO

This matters in codebases and libraries where interior mutability is the dominant pattern (e.g. GUI frameworks like leptos, dioxus, and gtk-rs, and certain game/engine styles) and most state sits behind Cell-like types. In those contexts, the difference between x += 1 and x.update(|x| x + 1) or *x.pawn() += 1 is amplified across thousands of call sites, and the boilerplate may accumulate and make syntax look a bit ugly

I’m not married to the exact syntax or trait names. The core question I’m interested in is whether a language-level, trait-based integration between interior-mut types and operators is something acceptable/unacceptable, or whether there’s a version of that idea that could be acceptable.

Discussion in that direction itself would be very helpful.

My biggest problem with Cell is that I generally can't use all those utilities available for usual primitive numbers. My concern is directly connected to Improving usability of having many nearly-identical methods - #10 by Vorpal , as it is a general pattern in Rust - when something is not ergonomic, we will just manually add methods to make something easier. And sometimes something gets missed, like Cell.

Thus, language support for Cell would help Cell and I would be glad about it.

1 Like