I think my biggest concern with the proposal is that the core claim of the trait - to separate ‘cheap’ from ‘not cheap’ - falls down when used under composition. Consider a smart pointer type, that internally holds reference counts to two other values:
struct Smart<T>(Rc<T>, Rc<Metadata>);
Should this implement Claim? Clearly, yes: two ref count increments is hardly expensive, and it makes ergonomic sense.
How about a struct like the following?
struct Pair<T: Claim, U: Claim>(T, U);
Sure, why not? Legitimate uses include those of the previous example. But now you open yourself up to throwing an arbitrarily large number of ref counts into the implementation of your claimable type. You’re back to square one: let x = y.claim()still doesn’t meaningfully tell you how expensive the operation is.
Except wait, it’s even worse! It’s not let x = y.claim(), it’s let x = y. The fact that this operation could be expensive is not only still there, the syntax sugar has actually worked to obfuscate this fact, disguising it as a simple assignment. I think this is fairly shaky territory for Rust, and this is particularly true because Claim will presumably be derived in 95% of cases.
I recognise that there’s an argument like ‘this is just Deref again!’, but the two are not the same: Deref‘s design is such that it’s extremely difficult to write an implementation that results in substantial work being performed because the returned reference must live as long as the self reference. This means that you can’t do the following to acquire that reference:
Call RefCell::borrow on an inner value
Call Mutex::lock/RwLock::read on an inner value
Perform any other expensive ‘guarding’ of the inner value
This matters, and I believe it’s one of the big reasons that you don’t see Deref abuse much in the wild.
I share the sentiment, which is why I think “claim” should be an explicit operator instead of an implicit call inserted by the compiler.
I don’t actually think “claim bloat” is going to be a thing, though. Stuff like that feels like a lot of stars have to align to make your type both made out of multiple layers of other types, and of other types that you don’t even know the internals of (which is especially rare since rust-analyzer shows you a type’s guts on hover). Largely, I don’t even trust foreign types in my Cloneable types unless I have inspected their internals and deemed them lightweight enough. I feel like the bloat concern is more about a general distrust in the community to self-regulate coding patterns, which they already do just fine with operator overloading and unsafe. I don’t believe it will be abused in the way people are concerned it might be. People are already self-conscious about even calling clone(), I feel like “claim” spam would be an even bigger faux pas.
To be clear, I’m not suggesting that deliberate or negligent abuse will be prevalent: Just that the fact that the trait is auto-derived means that it remains difficult to evaluate the cost of, and even more so with something as fundamental as assignment. This is already true today of Clone: it is difficult to evaluate the cost even when you’ve implemented it yourself.
This new articles reinforces my thoughts about the design:
Claim should not be a trait, but an annotation on Clone implementations, lest it inevitably ends up used in a bound where it shouldn’t
Tying the “automatic copyability” of types to their size is going to cause a lot of backward-compatibility issues because adding a Copy field to your struct won’t guarantee that it remains moveable. It also has weird interactions with future extensions of const generics (think [T; N])
The more I read about the design, the more I think that if the gain is just about clone ergonomics and preventing a performance pitfall, then the gain doesn’t outweigh the cost
It’s worrying this is trying to do too much at once. It’s grown from a small syntax convenience and performance lint to a whole set of requirements, including ones that can unexpectedly crash the whole program.
Claim can’t guarantee the operation is infallible. Rust has nothing usable for forbidding panics, especially in generic contexts. This means that Claim is fallible, it only makes the failures punished more severely.
It’s also incredibly dangerous to ever allow Claim to appear as a trait bound. This means that performance heuristics will not be possible to ever improve, because it would cause hard compilation errors. It will be a compat danger to use any 3rd party types, or even types that have a different size or alignment across platforms.
Library authors will have users requesting to add Claim for compatibility with other libraries having T: Claim bounds, and also having other users begging to remove Claim, because it’s crazy dangerous to have something activated implicitly that can cause an abort (which in places like kernels/drivers kills the whole machine).
“Expensive copy” should be a Clippy lint, not a whole new language semantic also tied to both type sizes and unwinding.
Copying of Cell can be a gotcha even when it’s expensive. Rust is missing a Freeze trait to control interior mutability.
Preventing panics is important in lots of places. This should be a separate fully-baked #[no_panic] feature. Limiting it to just a cheap copy lint is weirdly specific. For example, Deref should be enforced to be cheap and infallible too (or pure even).
I’ve been bitten by expensive memcpys in Rust, but two important cases where this happened were either with values that were nesting many different types (so no single type was really a clear culprit), or/and with types that aren’t even Copy. The memory copy happened on move! It seems like this part of the argument would be best served by a lint that flags expensive memcpy operation.
Rust is getting too complex as it is. This feature aims to solve the issue of cloning into closures, which admittedly can get quite verbose. It’s a good solution, and that’s why it’s hard to argue against it. The long term problem is that adding every feature that makes sense will eventually have Rust end up like C++.
I think the core premise of refining the distinction between Copy and Clone by introducing something that is basically a blend of the two, and providing some ergonomic benefits based on that, is certainly compelling. A couple thoughts:
I like that the compiler would try enforcing the infallibility property! However, doing so by forcing an abort doesn’t really seem to me like a benefit, and more of a punishment if somewhere in your code or that of your dependencies, a claimable type is panicking for some reason when it shouldn’t be. It does help with codegen/optimization, but that’s not really why infallibility is a nice property IMO.
One thing that I like about Copy, and that makes implicit copying feel safe, is that it really delivers on the promise of “transparent”. You know right away if a type is not Copy (the compiler will yell at you if you try to use it like one), the behavior is simple and crystal clear, and if you make a change that would remove the Copy property from a type, then not only do you get compile errors for all the places where you relied on that property, but any types that relied on that property lose their Copy status as well. The proposal states that Claim is transparent, but it definitely doesn’t feel as transparent as Copy, and not because Copy is just a memcpy, but because Claim does not come with any of the compiler support that Copy does, which ensure that as code evolves, you can count on the compiler to catch when what you thought was Copy, no longer is, and force you to fix it.
We’re missing a lint for passing around Copy values that are too large, but AFAICT that is well within the bounds of things that can be statically checked. With such a lint in place, all of the assumptions you might make about a Copy type would be compiler-checked - that feels like a necessary threshold to meet for compiler-inserted copies/claims/whatever, considering how frequent they are.
To those that say that things like Deref and others have the same issues, I don’t disagree in principle, but in practice they either have explicit syntax (e.g. the deref operator, explicit method calls), or by virtue of the trait itself, do not permit much in the way of opportunity for things to go sideways (as another commenter mentioned, it is hard to cause bad things to happen in Deref). Importantly, implicit copies happen all over the place, and the intent is to extend this to Claim-able types. I think that doing so should require that we try and meet the bar that Copy has set in terms of compiler-enforced bounds on what it means to be Copy (and in this case, Claim). If we can’t statically check that a Claim implementation is cheap (for some heuristic of cheap) and infallible, then IMO it is not transparent enough to be treated the same way Copy is.
I generally agree with the concerns posed by those who came before me. Especially this post by dureuill.
Beyond that, “and we ought to abort if it does” is something I’ll push back against heavily.
I came to Rust for the compile-time safety in a language where I can also easily expose bindings for languages like Python, I use #![forbid(unsafe_code)] and my biggest thing to fault it for has always been the lack of an official, maintained equivalent to abandoned tools like Rustig and findpanics. (My second-biggest is LLVM not giving it the tools it needs to make “this used to optimize and now it doesn’t” a compile-time error.)
To me, any proposal to add more things that a developer needs to get right and that’s reasoned as ““Infallible” ought to be “does not unwind” (and we ought to abort if it does)” strikes me as irresponsible and as something that, in practice will become “Increased chance of arbitrary third-party dependency X aborting and taking down your whole program unless you abandon things like Rayon and go for a multiprocess architecture to force the issue.”
One of Rust’s greatest strengths is its composability and I don’t want to be pushed to either reinvent wheels or go to the trouble of a multiprocess architecture just because the Rust equivalent to wrapping each unit of work in try:/except Exception: is no longer reliable.
Yes, I’m a user of one of the biggest “‘do what I mean’ at the expense of running fast if you have to” languages arguing against something proposed on ergonomic grounds. I’d much rather type a little more now than have to spend a lot more time later building my confidence that I’ve found all the bugs. Heck, that’s part of what defines Rust’s sweet spot. Unlike something like Coq, Agda, Idris, etc.
It makes you do a little more work to get a lot more certainty. As argued, this proposal feels like it’d save a little typing for significantly more uncertainty.
I’ll take performance pitfalls over greater chance of uncatchable panics in transitive dependencies any day.
This proposal seems quite good to me, but I know there were some people against it in the previous thread. I’ll be interested in seeing their thoughts on these clarifications.
I didn’t express an opinion under the original article because I was still mulling it over, but the more I think about it the more I like it. I find the argument that the current definition of Copy conflates two different notions of a type being “trivial” to copy (in terms of mechanics and cost, respectively) quite persuasive.
I wasn’t sure how to feel about claiming initially, but I actually like the idea. My biggest concern is people abusing it, especially because I feel it has a relatively niche use, but I think I trust the community enough to enforce proper use of it like they do now with operator overloading and unsafe.
My biggest issue with it is where the compiler will put control of the silent clone/claim call. I worry because this is an existing issue in C#, a type can define if it’s a class (always pass by reference) or a struct (always pass by value), which is pretty annoying because it means you have to define up-front if you want a pass-by-ref or pass-by-val type, and you don’t actually get any control of that when actually passing the type to a function. So my hope is that there will be a special claim operator akin to how we have the reference (&) operator now. I’m not a fan of “the compiler will decide.”
I’m not sure I understand your second paragraph. Seems like the compiler will always, for Claim types, insert a call to .claim() unless it is the vary last time that a variable is used. And if the type is not Claim, you get a compile error.
I’ve lamented in the past that something along the lines of Claim did not exist from Rust 1.0.
There is something to be said about the ability to say at compile time “copying this value doesn’t have considerable performance implications”.
I think my biggest concern with the proposal is that the core claim of the trait - to separate ‘cheap’ from ‘not cheap’ - falls down when used under composition. Consider a smart pointer type, that internally holds reference counts to two other values:
Should this implement
Claim? Clearly, yes: two ref count increments is hardly expensive, and it makes ergonomic sense.How about a struct like the following?
Sure, why not? Legitimate uses include those of the previous example. But now you open yourself up to throwing an arbitrarily large number of ref counts into the implementation of your claimable type. You’re back to square one:
let x = y.claim()still doesn’t meaningfully tell you how expensive the operation is.Except wait, it’s even worse! It’s not
let x = y.claim(), it’slet x = y. The fact that this operation could be expensive is not only still there, the syntax sugar has actually worked to obfuscate this fact, disguising it as a simple assignment. I think this is fairly shaky territory for Rust, and this is particularly true becauseClaimwill presumably be derived in 95% of cases.I recognise that there’s an argument like ‘this is just
Derefagain!’, but the two are not the same:Deref‘s design is such that it’s extremely difficult to write an implementation that results in substantial work being performed because the returned reference must live as long as the self reference. This means that you can’t do the following to acquire that reference:RefCell::borrowon an inner valueMutex::lock/RwLock::readon an inner valueThis matters, and I believe it’s one of the big reasons that you don’t see
Derefabuse much in the wild.I share the sentiment, which is why I think “claim” should be an explicit operator instead of an implicit call inserted by the compiler.
I don’t actually think “claim bloat” is going to be a thing, though. Stuff like that feels like a lot of stars have to align to make your type both made out of multiple layers of other types, and of other types that you don’t even know the internals of (which is especially rare since rust-analyzer shows you a type’s guts on hover). Largely, I don’t even trust foreign types in my Cloneable types unless I have inspected their internals and deemed them lightweight enough. I feel like the bloat concern is more about a general distrust in the community to self-regulate coding patterns, which they already do just fine with operator overloading and
unsafe. I don’t believe it will be abused in the way people are concerned it might be. People are already self-conscious about even callingclone(), I feel like “claim” spam would be an even bigger faux pas.To be clear, I’m not suggesting that deliberate or negligent abuse will be prevalent: Just that the fact that the trait is auto-derived means that it remains difficult to evaluate the cost of, and even more so with something as fundamental as assignment. This is already true today of
Clone: it is difficult to evaluate the cost even when you’ve implemented it yourself.This new articles reinforces my thoughts about the design:
Claimshould not be a trait, but an annotation onCloneimplementations, lest it inevitably ends up used in a bound where it shouldn’tCopyfield to your struct won’t guarantee that it remains moveable. It also has weird interactions with future extensions of const generics (think[T; N])The more I read about the design, the more I think that if the gain is just about clone ergonomics and preventing a performance pitfall, then the gain doesn’t outweigh the cost
It’s worrying this is trying to do too much at once. It’s grown from a small syntax convenience and performance lint to a whole set of requirements, including ones that can unexpectedly crash the whole program.
Claimcan’t guarantee the operation is infallible. Rust has nothing usable for forbidding panics, especially in generic contexts. This means thatClaimis fallible, it only makes the failures punished more severely.It’s also incredibly dangerous to ever allow
Claimto appear as a trait bound. This means that performance heuristics will not be possible to ever improve, because it would cause hard compilation errors. It will be a compat danger to use any 3rd party types, or even types that have a different size or alignment across platforms.Library authors will have users requesting to add
Claimfor compatibility with other libraries havingT: Claimbounds, and also having other users begging to removeClaim, because it’s crazy dangerous to have something activated implicitly that can cause an abort (which in places like kernels/drivers kills the whole machine).“Expensive copy” should be a Clippy lint, not a whole new language semantic also tied to both type sizes and unwinding.
Copying of
Cellcan be a gotcha even when it’s expensive. Rust is missing aFreezetrait to control interior mutability.Preventing panics is important in lots of places. This should be a separate fully-baked
#[no_panic]feature. Limiting it to just a cheap copy lint is weirdly specific. For example,Derefshould be enforced to be cheap and infallible too (or pure even).I’ve been bitten by expensive memcpys in Rust, but two important cases where this happened were either with values that were nesting many different types (so no single type was really a clear culprit), or/and with types that aren’t even
Copy. The memory copy happened on move! It seems like this part of the argument would be best served by a lint that flags expensive memcpy operation.Rust is getting too complex as it is. This feature aims to solve the issue of cloning into closures, which admittedly can get quite verbose. It’s a good solution, and that’s why it’s hard to argue against it. The long term problem is that adding every feature that makes sense will eventually have Rust end up like C++.
I think the core premise of refining the distinction between
CopyandCloneby introducing something that is basically a blend of the two, and providing some ergonomic benefits based on that, is certainly compelling. A couple thoughts:I like that the compiler would try enforcing the infallibility property! However, doing so by forcing an abort doesn’t really seem to me like a benefit, and more of a punishment if somewhere in your code or that of your dependencies, a claimable type is panicking for some reason when it shouldn’t be. It does help with codegen/optimization, but that’s not really why infallibility is a nice property IMO.
One thing that I like about
Copy, and that makes implicit copying feel safe, is that it really delivers on the promise of “transparent”. You know right away if a type is notCopy(the compiler will yell at you if you try to use it like one), the behavior is simple and crystal clear, and if you make a change that would remove theCopyproperty from a type, then not only do you get compile errors for all the places where you relied on that property, but any types that relied on that property lose theirCopystatus as well. The proposal states thatClaimis transparent, but it definitely doesn’t feel as transparent asCopy, and not becauseCopyis just a memcpy, but becauseClaimdoes not come with any of the compiler support thatCopydoes, which ensure that as code evolves, you can count on the compiler to catch when what you thought wasCopy, no longer is, and force you to fix it.We’re missing a lint for passing around
Copyvalues that are too large, but AFAICT that is well within the bounds of things that can be statically checked. With such a lint in place, all of the assumptions you might make about aCopytype would be compiler-checked - that feels like a necessary threshold to meet for compiler-inserted copies/claims/whatever, considering how frequent they are.To those that say that things like
Derefand others have the same issues, I don’t disagree in principle, but in practice they either have explicit syntax (e.g. the deref operator, explicit method calls), or by virtue of the trait itself, do not permit much in the way of opportunity for things to go sideways (as another commenter mentioned, it is hard to cause bad things to happen inDeref). Importantly, implicit copies happen all over the place, and the intent is to extend this toClaim-able types. I think that doing so should require that we try and meet the bar thatCopyhas set in terms of compiler-enforced bounds on what it means to beCopy(and in this case,Claim). If we can’t statically check that aClaimimplementation is cheap (for some heuristic of cheap) and infallible, then IMO it is not transparent enough to be treated the same wayCopyis.I generally agree with the concerns posed by those who came before me. Especially this post by dureuill.
Beyond that, “and we ought to abort if it does” is something I’ll push back against heavily.
I came to Rust for the compile-time safety in a language where I can also easily expose bindings for languages like Python, I use
#![forbid(unsafe_code)]and my biggest thing to fault it for has always been the lack of an official, maintained equivalent to abandoned tools like Rustig and findpanics. (My second-biggest is LLVM not giving it the tools it needs to make “this used to optimize and now it doesn’t” a compile-time error.)To me, any proposal to add more things that a developer needs to get right and that’s reasoned as ““Infallible” ought to be “does not unwind” (and we ought to abort if it does)” strikes me as irresponsible and as something that, in practice will become “Increased chance of arbitrary third-party dependency X aborting and taking down your whole program unless you abandon things like Rayon and go for a multiprocess architecture to force the issue.”
One of Rust’s greatest strengths is its composability and I don’t want to be pushed to either reinvent wheels or go to the trouble of a multiprocess architecture just because the Rust equivalent to wrapping each unit of work in
try:/except Exception:is no longer reliable.Yes, I’m a user of one of the biggest “‘do what I mean’ at the expense of running fast if you have to” languages arguing against something proposed on ergonomic grounds. I’d much rather type a little more now than have to spend a lot more time later building my confidence that I’ve found all the bugs. Heck, that’s part of what defines Rust’s sweet spot. Unlike something like Coq, Agda, Idris, etc.
It makes you do a little more work to get a lot more certainty. As argued, this proposal feels like it’d save a little typing for significantly more uncertainty.
I’ll take performance pitfalls over greater chance of uncatchable panics in transitive dependencies any day.
This proposal seems quite good to me, but I know there were some people against it in the previous thread. I’ll be interested in seeing their thoughts on these clarifications.
I didn’t express an opinion under the original article because I was still mulling it over, but the more I think about it the more I like it. I find the argument that the current definition of
Copyconflates two different notions of a type being “trivial” to copy (in terms of mechanics and cost, respectively) quite persuasive.I wasn’t sure how to feel about claiming initially, but I actually like the idea. My biggest concern is people abusing it, especially because I feel it has a relatively niche use, but I think I trust the community enough to enforce proper use of it like they do now with operator overloading and
unsafe.My biggest issue with it is where the compiler will put control of the silent clone/claim call. I worry because this is an existing issue in C#, a type can define if it’s a class (always pass by reference) or a struct (always pass by value), which is pretty annoying because it means you have to define up-front if you want a pass-by-ref or pass-by-val type, and you don’t actually get any control of that when actually passing the type to a function. So my hope is that there will be a special
claimoperator akin to how we have the reference (&) operator now. I’m not a fan of “the compiler will decide.”I’m not sure I understand your second paragraph. Seems like the compiler will always, for Claim types, insert a call to .claim() unless it is the vary last time that a variable is used. And if the type is not Claim, you get a compile error.
I’ve lamented in the past that something along the lines of
Claimdid not exist from Rust 1.0. There is something to be said about the ability to say at compile time “copying this value doesn’t have considerable performance implications”.