This discussion around &out
seems very similar to my RFC for &out T
and &uninit T
. What @newpavlov seems to be describing is similar to what I called &uninit
.
The main concern was that this introduced too many new pointer types, and that these pointer types were a bit complex (although that is specific to my proposal, a simpler proposal may get more traction).
After thinking about this topic for so long, my views have changed since I first made the RFC.
I don't think that &out
is a good idea because it I think it is misleading. It makes &T
look like a read only reference and &mut T
look like a read-write reference. But this way of thinking makes it significantly harder for new users to learn Rust. It is also wrong on the account of UnsafeCell
providing a way to mutate T
behind a &T
. So &T
doesn't actually mean read-only, and teaching that it means read-only is actively harmful.
Instead I have found that people understand the compiler better when they think in terms of uniqueness and compile time locks. &T
is a shared reference and that &mut T
is a unique reference. Then we explain that references are compile time locks, not simple pointers like from other languages. The fact that &T
disallows mutation by default is then easily explained as, shared mutability is a hard problem. Once people understand this, they start to understand the problems with their code better and understand why things are defined the way they are in std.
I have two examples that are made significantly more clear by thinking in terms of uniqueness.
Send
/Sync
The blanket impl for Send
/Sync
is as follows,
unsafe impl<T: Sync + ?Sized> Send for &T {}
unsafe impl<T: Send + ?Sized> Send for &mut T {}
If you look at this through the lens of &T
is a read only reference and &mut T
is a read-write reference, this makes no sense. Why do &T
and &mut T
have different bounds for Send
? It is unclear because there is some very important information missing.
If you look at this through the lens of &T
is a shared reference and &mut T
is a unique reference, then things become more clear. Sync
now carries the meaning that if T
can be safely shared across threads then a shared reference to T
can be sent across thread boundaries. The bound for &mut T
also becomes clear, as you can safely send a unique reference to T
across thread boundaries if you can send T
across threads, because &mut T
is a unique compile time lock to T
, so it behaves just like T
with few exceptions.
IterMut
The other example I have of how thinking in terms of uniqueness improves understanding is IterMut
. Specifically how it requires unsafe
to implement for non-trivial collections. This as opposed to Iter
which does not require unsafe
to implement for non-trivial collections.
Because &mut T
is a unique compile time lock and Iterator::collect
is safe, you must be able to prove that all &mut T
yielded by the iterator are completely disjoint. This is a non-trivial condition and Rust can't prove this for most collections's implementation of IterMut
.
An example of a type which could implement IterMut
without any unsafe
would be a naive linked list. This is because the Rust compile can prove that all yielded &mut T
are indeed unique. But this is the simplest data structure you could make, pretty much every other data structure must use unsafe
(or ride off of some other unsafe
implementation of IterMut
) because Rust cannot prove that the yielded &mut T
are indeed unique.
Using another implementation of IterMut
shifts the burden of proof to the other implementation, so it doesn't really invalidate my point. (yes collections are usually implemented on top of raw pointers which need unsafe
anyways, but this also doesn't detract from my point because Rust still can't prove that you are yielding disjoint &mut T
).
Because of this, I think it is more important to teach in terms of uniqueness and build constructs around this idea instead of in terms of mutability (which is just a side-effect of safe defaults).
This is something I also thought when I was writing the RFC, but I think it is flawed because we already have completeness, you have either a shared reference or a unique reference, you can't have something that is not shared and not unique (unless it is inaccessible, which is useless), or both shared and unique (That just doesn't make sense). So we have all of the references we need (actually we are still missing one, &own T
, which takes ownership of T
and allows cheaply owning unsized types).