Pre-RFC: Assign `Copy` types to `Cell` without `set()`

TL;DR allow fn f(x: &Cell<u32>) { *x = 1 }

I just did a large refactoring with Cell, changing assignments to use get() and set() instead. Being forced to use get() and set() when the types are Copy feels like meaningless churn, and perhaps an unnecessary deterrent from using Cell. Cell should just stay out of my way unless I try to do something wrong.

Could we add support for using the assignment operator for Cell<T> where T: Copy? Or perhaps allow coercion between Cell<T> and T, which would also allow assignment in both directions?

The current semantics of unary * are such that every mutating usage of it implies the ability to get a &mut reference to the same place (via DerefMut::deref_mut) — but getting &mut T from &Cell<T> is unsound.

What extension do you propose to the language * such that the library Cell can treat it like set() instead?

6 Likes

In my initial example the * only derefs &Cell to Cell. I'm not proposing that derefing a Cell should be allowed.

As for the actual language extension, the user-level explanation is that assignment of T to Cell<T> "just works". Implementation-wise, there would have to be a special case in typeck to coerce the left hand side from Cell<T> to &mut T. Once typeck passes, we're out of the woods because Cell is repr(transparent).

Like IndexMut has proved to be arguably insufficient and should be supplemented by IndexSet, perhaps the same should be done for DerefMut. After all, indexing and dereferencing are very much analogous.

5 Likes

So as I understand it, the proposal is to make

(shared place at type Cell<T>) = (value at type T)

"just work" when T: Copy (i.e. you could use .set).

While this is a coherent feature to make working with Cell less cumbersome, I think I'm -0 on it as a feature, because it's making Cell more special for nonspecific gain.

However, if combined with the theoretical language support for "Cell projection" (that is, thing.field works on Cell<Thing> (place) and gives Cell<Field> (place)), then I'm +¾ on cell assignment on top of that.

1 Like

The projection idea is interesting. We can also add operators like +=. Though this may be possible by just adding impls.

This is unsound. The problem is that assigning to an &mut T first drops the old T and then writes the new one. This is problematic because T receives an &mut T in its drop implementation, but while that exists it could access the Cell<T> you're writing to, thus creating aliasing references. For comparison, Cell::set solves this by first replacing the T inside and then dropping it.

I'm also not that enthusiastic about special casing yet another stdlib type. I would prefer something any user can implement for its own Cell-like type.

10 Likes

I don't think these semantics are quite fully fleshed out; is this a requirement for Copy types?

Oh right, I forgot for a moment this is restricted to Copy types. Copy requires !Drop so it should be fine.

1 Like

This. I'm not at all convinced "makes refactoring somewhat shorter-looking" is a valid argument at all for a new language feature and the special-casing of a library type.

4 Likes

I get that special casing a library type isn't something to take lightly. But Cell isn't "just" a library type in the same way as Option or Vec. It is more similar to core::marker::* traits which enable a primitive language feature for a type. Cell enables interior mutability. So I think it is good for it to behave like a marker rather than a container in cases where that behavior can be provided coherently.

Actually, the language item is UnsafeCell. Cell and Mutex are then built on top of UnsafeCell with no special language semantics at all.

12 Likes

I don't really see why the Copy bound is relevant, here? It would, however, be, for the symmetrical operation: that of making let value = *x; Just Work™ as well.

I wonder if this couldn't start prototyped as a proc-macro first, transforming all the <place> = <value>;s into <place>.set(<value>), with an added extension trait or something to handle the &mut case as well :thinking:

1 Like

In fact, if you had such a trait you may not even need IndexSet. Suppose you hand this trait:

// Sugar for:
// *prt = value;
trait DerefSet<Inner> {
    fn deref_set(&mut self, val: Inner);
}

Then a map could return a reference to a smart pointer that implements Deref, DerefMut and DerefSet.Then you would be able to write something like this:

*map[key] =value;
// As sugar for this
(*map.index_mut(key)).deref_set(value)
// here index_mut returns something like `&mut OptionRefMut<V>`

Now it would be nicer if IndexMut wasn't limited to returning a reference, and it could return any smart pointer.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.