I'm particularly interested in the cognitive load their differences places on developers. One has to really know how the type will behave when they are reading code to know what is really going on. For example:
let x = a;
let y = x;
Given this example, you need to know if a has the Copy trait to know how it will behave and whether or not you can continue using it.
If Move was the default behavior, you would know that x is invalidated upon the let y = x assignment. This would be consistent for all variables. Likewise, you could always explicitly copy it:
let x = 5;
let y = x.clone();
Then, you would be able to keep using x.
A counter-argument
When you see a call to clone , you know that some arbitrary code is being executed and that code may be expensive. It’s a visual indicator that something different is going on.
It seems like this would lose some value, since scalar values would be using clone as well.
When considering the performance here, you would need to take on the burden that is removed above. It's during these performance considerations that you would need to think about the type and what clone really means for that type. Nevertheless, it seems to make the most sense to be giving more thought to the internal workings of a copy when considering performance than when tracking the scope of variables.
I’m not sure if this is a misunderstanding or just a difference of opinion, but I would say that move is the default.
Copy is a special property of a type that means it can still be used after its data has been reused elsewhere. If you write code that would compile without the existence of the Copy trait, then it will compile anyway. But if you mistake a non-Copy type for a Copy type, the compiler will complain, and you’ll immediately see that the type is being moved rather than copied.
It may help to consider what is actually happening at runtime. Unlike in C++, “moving” a value is copying it; and “invalidating” values is done entirely at compile-time. In any language, copying and reusing a value with references in it would require those references to be shared with the copy. But because Rust enforces single-ownership by default, it must also (by default) prevent such copy-and-reuse semantics. Hence the default semantics really are “move” based.
Note that there's no observable difference between copying and cloning if you don't try to use the old thing. So if you aren't interested about the old one, you can just not care.
And if you are interested, you can just .clone() it. clippy::clone_on_copy will tell you that you didn't need it, if you then want to clean things up.
You're absolutely right! It is the default. I was trying to suggest scalars and such should not have the Copy trait. I should have said that I believe Copy as a special property of a type should not be possible.
This is where the cognitive load is coming in. When looking at the code, you have to know details about the type that aren't visible.
Reading between the lines a bit, it sounds like you see this as creating a small cognitive load when writing/modifying the code, but not when reading code. It's small because you can lean on clippy.
If it really doesn't add to the cognitive load when reading code, then the added value of having .clone() only appear when it's potentially expensive is a net gain.
Removing Copy is a breaking change that can't be avoided with an edition. Moreover it is a huge ergonomic hit. Think about numerics heavy code (like in machine learning or data processing) , having to write out .clone() would be a huge burden and would kill the readability of the code.
Copy shows that the type is just data without any resources that the compiler is unaware of (i.e. it doesn't implement Drop and it has no drop glue).
Copy tells that making a copy is very fast, unlike with Clone (so making clone required for all types makes it harder to find where the expensive clones are).
there are exceptions where Copy is expensive when the types are large, but this is rare, and Copy will never be more expensive then Clone
This suggestion is also completely contrary to the current standard as per the docs for Copy
Generally speaking, if your type can implement Copy , it should. Keep in mind, though, that implementing Copy is part of the public API of your type. If the type might become non- Copy in the future, it could be prudent to omit the Copy implementation now, to avoid a breaking API change.
On the note of the cognitive load, I find that most people don't really think about Copy until the compiler complains about it. So it doesn't really cost that much. But losing Copy has a lot of costs, as I outlined above.
Not really, if you see the same binding twice you can rest assured that the type is either Copy or a &mut T (the latter due to re-borrowing). And in either case it is just a mem-copy, so there is a negligible performance cost.
I would argue, that most of the time you don’t have to know and
therefore there’s no higher cognitive load.
Normally both operations - cloning and copying - are very cheap, so you
don’t have to care. You only have to care if you’re getting ownership
issues, but then there’s the borrow checker to help you out.
I think that’s a pretty nice example how hard language design is,
because especially for a systems language a lot of people would argue
that control and explicitness are the most important things. Well,
yes they’re in a way, but only if they matter and make a real
difference, but these things can’t be determined by just theoretical
discussions, but have to be practically tried out.
@dan_t For me, I’ve also learned that it also really highlights the need to be clear with yourself about what the issue is vs what the solution is. I started this to address the inconsistency of invalidation after assignment, rather than the difference between copy and clone. While they are closely related, I missed the subtle distinction.
Nevertheless, I learned a great deal from this thread. I’m looking forward to learning rust in- depth, and contributing as best I can.