Rust syntax for Cell/RefCell

so I've been thinking about some rust syntax for some cell types, and I wanted to see what you guys think.

I am not 100% sold into this design, but I hope to at least have a discussion of possible designs for this, as I think cell.set(cell.get() + 1) doesn't feel and look as good as the typical *num += 1

Now, for the design, we could have something like

impl ShrAddAssign<T: Copy + Add<T>> for Cell<T>{
    fn shr_add_assign(&self, input: T){
        self.set(self.get() + input)
    }
}
impl ShrAddAssign<T: Copy + AddAssign<T>> for RefCell<T>{
    fn shr_add_assign(&self, input: T){
        *self.borrow_mut() += input;
    }
}
impl ShrAssign<T: Copy> for Cell<T>{
    fn shr_assign(&self, input: T){
        self.set(input)
    }
}
impl ShrAssign<T: Copy> for RefCell<T>{
    fn shr_assign(&self, input: T){
        *self.borrow_mut() = input;
    }
}
impl ShrInner<T: Copy> for Cell<T>{
    fn shr_inner(&self) -> T{
        self.get()
    }
}
impl ShrInner<T: Copy> for RefCell<T>{
    fn shr_inner(&self) -> T{
        *self.borrow()
    }
}

and then u can do

let cell : &Cell<i32>= &Cell::new(4);
*cell := 5; // ShrAssign
*cell :+= 5; // ShrAddAssign
let number : i32 = ^cell; // ShrInner. 

I've also thought, that maybe for something like ShrAddAssign, it can use the same += syntax instead of :+= and then there would be a blanket implementation that look like this:

impl<T: ShrAddAssign<U>, U> AddAssign<U> for T{}
1 Like

This works for very simple updates, but anything more complicated would probably need a more complete update function, which in any case exists. The example is pretty much your increment example:

use std::cell::Cell;

let c = Cell::new(5);
c.update(|x| x + 1);
assert_eq!(c.get(), 6);
5 Likes

by complex, I am assuming you mean something like

let cell = &Cell::new(Vector3::new(0,1,2)); // Vector3 as in, linear algebra Vector 3
cell.update(|mut i| { i.x = 5; i })

?

if so, now that you mention it, I realized that my design does not cover it, but the field projection RFC does, which is something that is currently being taken more seriously, I believe?

With field projections, it could look like

let cell = &Cell::new(Vector3::new(0,1,2)); // Vector3 as in, linear algebra Vector 3
*cell->x := 5;
1 Like

Standard response: what you've proposed here is a solution, but you should start with a solid problem description. What's the pain you're trying to address, what do you have to do today, why are those things unacceptable, and if there are alternatives today, why is what you're doing common enough to be worth adding language complexity to address?

That gives the opportunity for others to come up with lots of different possible solutions, instead of just looking at one.

9 Likes

The pain I'm trying to address is pretty much the ergonomic gap betweem a normal &mut operations compared to Cell/RefCell.

In some rust code that rely in interior mutability, like code that you typically see in libraries like gtk-rs and leptos, the operations are typically simple. incrementing a counter and stuff like that, but the syntax is heavier than the equivalent when u have a &mut value

num += 4; is much shorter than num.set(num.get() + 4)/num.update(|x| x + 1)

When it shows up once in a while its fine. But for codebases that rely heavily on interior mutability, it can start to look a bit ugly and verbose.

The goal of my proposal is to see if its possible to come up with sane syntax that is similar to that of &mut operations, so codebases that heavily rely on interior mutability will be less verbose.

you can wrap it in a newtype: Rust Playground

3 Likes

I believe with field projections it would be something like cell->x.set(5), that is the field projection would give you a &Cell to the field.

I would say that this is more of a hack than a proper design solution, as u need to put the "mut" on the structure, which is something that IMO shouldn't be required for interior mutability.

yes but u can still find urself writing this

cell->x.set(cell->x.get() + 1)

when you could possibly write something like this

cell->x :+= ^cell->x

or

cell->x += ^cell->x

no mut in user code: Rust Playground

(deref shenanigans can be refined better)

*cell.wrap() += 27;

I still believe that this is hacky, and u still have to call wrap(), which does not feel as ergonomic as

*num += 27

Well, with no language support at all, pawn — Rust library // Lib.rs lets you do

let c = Cell::new(4_u32);
*c.pawn() += 4;

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=fd31b29e756214a9cc507d4532025d5a

Even lets you do things like cell_of_vec.pawn().push(123); despite Vec not being Copy.

I don't think it's that bad to say that you need to write more when it's a Cell.

3 Likes

I guess it is not that bad, and it certainly is not a major issue, but I was hoping for something that doesn’t require an extra “opt-in” call like pawn()/wrap() at each use site.

The thing I’m trying to get at is that in code that already leans heavily on interior mutability, having to sprinkle those helper calls everywhere makes the code feel more cluttered than the equivalent *num += 1 on a normal &mut. Though I would understand if one may argue that extra syntax in the language isn’t worth the added complexity just to avoid a method call(s) for only two types.

I still decided to propose this anyways, to see if there are other people that would want this feature. But if it is something that no one really wants, I understand.

I'd be interested to hear more about what you're doing that has you leaning in that direction. Knowing why you feel you have to use so many Cell might make people more receptive to the syntactic overhead actually being a problem worth doing something about.

I experiment and make some toy projects with GUI frameworks like leptos and gtk-rs, which typically encourage interior mutability a lot.

gtk-rs documentations encourage code to be written like this

let number = Cell::new(0);

button_increase.connect_clicked(move |_| {
    number.set(number.get() + 1);
});

and in leptos, the example below taken from leptos documentation

use leptos::prelude::*;

#[component]
fn App() -> impl IntoView {
    let (count, set_count) = signal(0);

    view! {
        <button
            on:click=move |_| set_count.set(3)
        >
            "Click me: "
            {count}
        </button>
        <p>
            "Double count: "
            {move || count.get() * 2}
        </p>
    }
}

count uses interior mutability in case you haven't noticed

For projects with little state, it looks fine. But when the project gets larger, a user might find themselves having to use more state, which means, more interior mutability state. Which means, more interior mutability syntax overhead, which can cause the code to start to look a bit boilerplate-y

And also, there are patterns in gamedev that are not ECS, that rely on shared mutable state, like how some c++ games are programmed. Rust can express this. Rust gives you the interior mutability to do so, but it starts to look ugly when you sprinkle a lot of cell.set(cell.get() + 2) or *refcell.borrow_mut() = some_instance.

There was an article of a user that left rust gamedev due to it being quite difficult to prototype with. “Leaving Rust gamedev after 3 years”. The post is basically written from the perspective of “I want to prototype and ship games quickly, and Rust keeps pushing me into big refactors / ECS / context objects whenever I try to add new gameplay.” A lot of that friction is about working with the borrow checker and the architectures it nudges you toward, rather than about raw performance.

My hunch is that having a nice syntactic path for interior mutability (as opposed to leaning on ECS everywhere) could make those more prototype-friendly game designs less painful. Not saying ECS is bad. it’s a great and well structured pattern. But there’s a whole class of game code that really just wants “I have a bunch of objects with shared state. I want to be able to mutate x object while holding a mutable y object without reentrancy issues. I don't want to have to make a whole new system just to add a new feature. I just want to make stuff rather than fight aliasing issues"

1 Like

Wouldn't a solution to this be a newtype wrapper around a Cell that has impls of Add<UnderlyingType>, AddAssign<UnderlyingType> etc. That should get you pretry close. You still need .set() as assignment itself can't be overloaded (which I'm thankful for, it is a confusing misfeature in C++).

1 Like

There is still some boiler plate to that, like trying to convert &Cell<T> into Wrapper<T>, every time u want to do some kind of operation, and then u need to mark Wrapper<T> as mut, to be able to use the += like syntaxes, which feels weird for something that is designed to be mutable without mut

No? Just use Wrapper<T> everywhere. Store wrappers not direct Cells in your structs in the GTK bindings etc. If you use repr(transparent) this should even work with FFI. No conversion involved.

Can you write some code in rust playground for me to see what you mean exactly?

After trying ur suggestion, it still doesn’t really solve the syntax overhead problem. Using Wrapper instead of Cell won’t automatically make stuff like wrapper += 5 actually work, because again, you need mut for that syntax, and people typically don’t have mut when working with interior mutability.

All the suggestions I have seen are mostly workarounds and still don’t have the natural feel of syntax like num += 5. That’s why I have made this proposal. I don’t really think a user should have to use a crate or a custom type to have something that’s only a bit better syntactically wise for types like Cell/RefCell, which has the potential to be used a lot in certain types of patterns