InteriorAssign trait { a := b }

There are already traits such as AddAssign to implement +=.

It would be useful for code clarity if you could implement InteriorAssign to use :=.

This could be used to sugar syntax such as this:

data.ref_cell.replace(Some(new_value))

into this:

data.ref_cell := Some(new_value)

Which I personally find easier to read. And there is no ambiguity with =


The trait could look something like this:

pub trait InteriorAssign<Rhs = Self> {
    fn assign(&self, rhs: Rhs);
}

Note the &self to allow this to be used for interior mutability.


There could be InteriorOpAssign versions that use &self for all the other operators as well:

InteriorAddAssign => :+=

InteriorSubAssign => :-=

Etc...


Bonus

As a bonus, (I'm not sure this can work) implement InteriorAccess to use ->

So instead of:

ref_cell.borrow_mut().some_method()

You can use:

ref_cell->some_method()

It might cause issues for the parser because it is used for function returns, but if not then this syntax could cut down a lot on a lot of boilerplate working with interior mutability.

Other symbol options:

ref_cell~>some_method()

ref_cell:.some_method()

ref_cell:->some_method()

We don’t even have traits for customizing “assignment” operations for the non-interior-mutable case; we also don’t have special syntax for borrow_mut()-style projections in the non-interior-mutable case.

A main design choice of Rust is to keep interior mutability limited / the exceptional case, as far as resonably possible; the syntax overhead is not a limitation, it’s intentional.

10 Likes

Why would you need it for those cases? You can just a regular = for those.

Please can you link to where it says this?

It's pretty much baked into the foundational philosophy of the language, as evidenced by the fact that regular mutability has syntax support (and has had it since before 1.0) where interior mutability does not.

That syntactic divide was no accident. It follows from a recognition that interior mutability occupies a point on a spectrum between "useful" (eg perf optimization) and "necessary" (in the occasional case where regular CREW semantics are too restrictive).

2 Likes

Right, but the behavior of = is fixed and not overloadable. Yet, overloadable assignment-like operations for a neat shorter syntax would be something that users might want to use. And if we offer it only for interior-mutable primitives, it’d be like encouraging interior mutability onto certain APIs only for the sake of syntactic concisenes.

On the other hand, there are good reasons in favor of keeping assignment non-overloaded; ultimately that’s the only way to really guarantee to all readers of Rust code that writing = really is generally a rather cheap operation.

2 Likes

To be clear I am not proposing overloading = at all. You keep mentioning other things, but they have nothing to do with what I am proposing.

You also still haven't provided the link I requested.

This is circular.

You could literally reject all feature requests by saying it was not a feature from the start, at which point, why even have this forum?

It would be a circular argument if the goal was to argue that this should continue to be part of Rust’s philosophy. However, for the goal of saying what is currently the case, it doesn’t seem circular. Do keep in mind, of course, that breaking from the current ethos of the language requires a fairly strong motivation to overpower concerns about churn or confusion.

3 Likes

Well… we’re talking not about just any language feature, but specifically about new operators. New syntax like that has a higher bar for being considered, and thus it’s important to consider all potential downsides / counter-arguments. Also compare this document [in particular this section] for some more documented thoughts/principles, also touching on things like “new operators”.

4 Likes

Thanks for taking the time to post the link.

Changes that would break existing Rust code are non-starters.

As far as I can tell, my := proposal would not cause any breaking changes.

My -> proposal I am unsure about.

For instance, we don’t want to make changes that make code easier to write but harder to read, or changes that make code more error-prone to modify and maintain.

I feel like my proposal makes it both easier to read and write code that deals with internal mutability.


If it's not too much work, I might have a go at implementing this myself so people can try it first.

RefCell is a good motivating example, but I don't think this needs to be limited to types with interior mutability:

pub trait InteriorAssignMut { /*...*/ }
pub trait InteriorAssign: InteriorAssignMut { /*...*/ }

Containers like Option could implement only InteriorAssignMut:

let mut num = Some(12);
num := 13;

Would this resolve your concern, @steffahn?

I am not proposing for this use case.

For completeness, the current way to do this would be:

num = Some(13)

And another way to do this currently is

num = 13.into();

By the way, a current way to “assign” (rather than “replace”, i.e. not getting back the previous value as an output) inside of a RefCell, that actually uses the assignment operator, would be

*data.ref_cell.borrow_mut() = Some(new_value);

instead of the

data.ref_cell.replace(Some(new_value));

mentioned in the original post of this thread.

Given mem::replace is must_use already, this makes me wonder if RefCell::replace should be must_use, too.

3 Likes

It could be defined to do that, or it could be defined to do:

if let Some(num) = &mut num { *num = 13; }

or

*num.as_mut().unwrap() = 13;

I didn't really mean to use Option as a motivating example, just a simple one.

Thanks, that is really good to know.

A better example might be Cell, since it doesn't let you do that. I imagine this feature coming into play whenever you have a type that doesn't want to hand out references to its interior but still wants to allow you to mutate it. C# properties are a similar thing (and maybe something closer to those would be a better idea, idk).

Another use case for an overloadable assignment unrelated to interior mutability is inserting items into a HashMap, which currently cannot be done using map[key] = ... syntax, because it could not avoid panicking on an absent key, because right now the only trait for this is IndexMut which requires returning &mut to an existing, initialized place. In general, Rust assignment currently demands &mut, but producing &mut is not always possible or wise even when an assignment-like operation is possible.

4 Likes

Thinking about it... could we maybe just use Try operator to implement everything I talked about?

I'm going to play around with this but maybe something like this is possible:

data.ref_cell? = Some(new_value)

What if it was setup to allow Try Operator to get the mutable reference?

map[key]? = value