9 ms·
# [derive(Clone)] Is Broken
- Kenji 1y ago[dead]
- hyperbrainer 1y ago> we cannot just require all generic parameters to be Clone, as we cannot assume they are used in such a way that requires them to be cloned. I don't understand what "used in such a way requires them to be cloned" means. Why would you require that?
- rocqua 1y agoA type might have a generic parameter T, but e.g. use it as a phantom marker. Then even if T isn't cloneable, the type might still admit a perfectly fine implementation of clone.
- xvedejas 1y agoRight now the derive macro requires `T` be `Clone`, but what we actually want to require is only that each field is clone, including those that are generic over `T`. eg `Arc<T>` is `Clone` even though `T` isn't, so the correct restriction would be to require `Arc<T>: Clone` instead of the status quo which requires `T: Clone`
- tinco 1y agoThat's the crux of the article. There's no good reason for this requirement, at least none that arise in the article, so the author concludes it must be a mistake. I think it's a bit cynical that it would cost at least 4 years for this change to be admitted into the compiler. If the author is right and there is really no good reason for this rule, and I agree with the author in seeing no good reason, then it seems like something that could be changed quite quickly. The change would allow more code to compile, so nothing would break. The only reason I could come up with for this rule is that for some other reason allowing non complying type parameters somehow makes the code generation really complex and they therefore postponed the feature.
- the_mitsuhiko 1y ago> The only reason I could come up with for this rule is that for some other reason allowing non complying type parameters somehow makes the code generation really complex and they therefore postponed the feature. The history of this decision can be found in details in this blog post: https://smallcultfollowing.com/babysteps//blog/2022/04/12/implied-bounds-and-perfect-derive/ https://smallcultfollowing.com/babysteps//blog/2022/04/12/im... The key part: > This idea [of relaxing the bounds] is quite old, but there were a few problems that have blocked us from doing it. First, it requires changing all trait matching to permit cycles (currently, cycles are only permitted for auto traits like Send). This is because checking whether List<T> is Send would not require checking whether Option<Rc<List<T>>> is Send. If you work that through, you’ll find that a cycle arises. I’m not going to talk much about this in this post, but it is not a trivial thing to do: if we are not careful, it would make Rust quite unsound indeed. For now, though, let’s just assume we can do it soundly. > The other problem is that it introduces a new semver hazard: just as Rust currently commits you to being Send so long as you don’t have any non-Send types, derive would now commit List<T> to being cloneable even when T: Clone does not hold. > For example, perhaps we decide that storing a Rc<T> for each list wasn’t really necessary. Therefore, we might refactor List<T> to store T directly […] We might expect that, since we are only changing the type of a private field, this change could not cause any clients of the library to stop compiling. With perfect derive, we would be wrong.2 This change means that we now own a T directly, and so List<T>: Clone is only true if T: Clone.
- josephg 1y agoYeah; I think this argument makes sense. With perfect derive, #[derive(Clone)] has a bunch of implicit trait bounds which will change automatically as the struct changes. This has semver implications - and so we might want to be explicit about this rather than implicit. We could solve this by having developers add trait bounds explicitly into the derive macro. Currently this: #[derive(Clone)] struct Foo<T>(Arc<T>) expands to: impl Clone for Foo where T: Clone { ... } Perfect derive would look at the struct fields to figure out what the trait bounds should be. But it might make more sense to let users set the bound explicitly. Apparently the bon crate does it something like this: #[derive(Clone(bounds(Arc<T>: Clone)))] Then if you add or remove fields from your struct, the trait bounds don't necessarily get modified as a result. (Or, changing those trait bounds is an explicit choice by the library author.)
- moomin 1y agoHaskell does this. If you derive Eq, it puts a condition on the generic parameter(s) requiring them to be Eq as well. Then if you use it with something that doesn’t implement Eq, your generic type doesn’t either. It helps if you can express these preconditions in the first place, though.
- hardwaresofton 1y agoHaskell has, like-for-like, a better type system than Rust. That said, Rust is just enough Haskell to be all the Haskell any systems programmer ever needed, always in strict mode, with great platform support, a stellar toolkit, great governance, a thriving ecosystem and hype. Lots of overlap in communities (more Haskell -> Rust than the other way IMO) and it's not a surprise :)
- 01HNNWZ0MV43FF 1y agoI have respect for Haskell because I saw it long before I saw Rust, and I love the ideas, but I never got around to actually using Haskell. I think an IO monad and linear types [1] would do a lot for me in a Rust-like [1] Affine types? Linear types? The ones where the compiler does not insert a destructor but requires you to consume every non-POD value. As someone with no understanding of language theory, I think this would simplify error handling and the async Drop problem
- hardwaresofton 1y agoI used Haskell for a while and eventually switched over (and intentionally target Rust for many new projects, though I suspect I should be doing more Go for really simple things that just don't need more thought). One large problem with Haskell that pushes people to Rust is strictness, I think -- laziness is a feature of Haskell and is basically the opposite direction from where Rust shines (and what one would want out of a systems language). It's an amazing feature, but it makes writing performant code more difficult than it has to be. There are ways around it, but they're somewhat painful. Oh there's also the interesting problem of bottom types and unsafety in the std library. This is a HUGE cause of consternation in Haskell, and the ecosystem suffers from it (some people want to burn it all down and do it "right", some want stability) -- Rust basically benefited from starting later, and just making tons of correct decisions the first time (and doing all the big changes before 1.0). That said, Haskell's runtime system is great, and it's threading + concurrency models are excellent. They're just not as efficient as Rust's (obviously) -- the idea of zero cost abstractions is another really amazing feature. > [1] Affine types? Linear types? The ones where the compiler does not insert a destructor but requires you to consume every non-POD value. As someone with no understanding of language theory, I think this would simplify error handling and the async Drop problem Yeah, the problem is that Affine types and Linear types are actually not the same thing. Wiki is pretty good here (I assume you meant to link to this): https://en.wikipedia.org/wiki/Substructural_type_system https://en.wikipedia.org/wiki/Substructural_type_system Affine is a weakening of Linear types, but the bigger problem here is that Haskell has a runtime system -- it just lives in a different solution-world from Rust. For rust, Affine types are just a part of the way they handle aliasing and enable a GC-free language. Haskell has the feature almost... because it's cool/powerful. Yes, it's certainly useful in Haskell, but Haskell just doesn't seem as focused on such a specific goal, which makes sense because it's a very research driven language. It has the best types (and is the most widely adopted ML lang IIRC) because it focuses on being correct and powerful, but the ties to the ties to practicality are not necessarily the first or second thought. It's really something that Rust was able to add something that was novel and useful that Haskell didn't have -- obvious huge credit to the people involved in Rust over the years and the voices in the ecosystem.
- lidavidm 1y agoSomeone on the issue they made explained why Clone is "broken": https://github.com/JelteF/derive_more/issues/490#issuecomment-3037408110 https://github.com/JelteF/derive_more/issues/490#issuecommen... Which links to this blog post explaining the choice in more detail: https://smallcultfollowing.com/babysteps/blog/2022/04/12/implied-bounds-and-perfect-derive/ https://smallcultfollowing.com/babysteps/blog/2022/04/12/imp...
- deleted 1y ago[deleted]
- sbt567 1y agofrom Niko's post: > In the past, we were blocked for technical reasons from expanding implied bounds and supporting perfect derive, but I believe we have resolved those issues. So now we have to think a bit about semver and decide how much explicit we want to be.
- samsartor 1y agoI have a crate with a "perfect" derive macro that generates where clauses from the fields instead of putting them on the generic parameters. It is nice when it works, but yah cyclical trait matching is still a real problem. I wound up needing an attribute to manually override the bounds whenever they blow up: https://docs.rs/inpt/latest/inpt/#bounds https://docs.rs/inpt/latest/inpt/#bounds
- mmastrac 1y agoI did a similar thing for derive-io. It greatly improved the ergonomics of the macro. https://docs.rs/derive-io/latest/derive_io/ https://docs.rs/derive-io/latest/derive_io/ Being able to handle directly recursive type bounds would be an awesome improvement to the compiler, IMO.
- loa_in_ 1y agoAutomatically deriving Clone is a convenience. You can and should write your own implementation for Clone whenever you need it and automatically derived implementation is insufficient.
- josephg 1y agoBut this issue makes it confusing & surprising when an automatically derived clone is sufficient and when its not. Its a silly extra rule that you have to memorise. By the way, this issue also affects all of the other derivable traits in std - including PartialEq, Debug and others. Manually deriving all this stuff - especially Debug - is needless pain. Especially as your structs change and you need to (or forget to) maintain all this stuff. Elegant software is measured in the number of lines of code you didn't need to write.
- 0rzech 1y agoIt's surprising up to the moment the compilation error tells you that all of the members have to implement derived trait. Nevertheless, it would be cool to be able to add #[noderive(Trait)] or something to a field not to be included in automatic trait implementation. Especially that sometimes foreign types do not implement some traits and one has to implement lots of boilerplate just to ignore fields of those types. I know of Derivative crate [1], but it's yet another dependency in an increasingly NPM-like dependency tree of a modern Rust project. All in all, I resort to manual trait implementations when needed, just as GP. [1] https://crates.io/crates/derivative https://crates.io/crates/derivative
- 0rzech 1y agoApparently, Derivative is unmaintained [1], but there is Derive_more [2], Educe [3] and Derive-where [4], if anyone is interested. [1] https://rustsec.org/advisories/RUSTSEC-2024-0388.html https://rustsec.org/advisories/RUSTSEC-2024-0388.html [2] https://crates.io/crates/derive_more https://crates.io/crates/derive_more [3] https://crates.io/crates/educe https://crates.io/crates/educe [4] https://crates.io/crates/derive-where https://crates.io/crates/derive-where
- deleted 1y ago[deleted]
- deleted 1y ago[deleted]
- qwertox 1y agoLLMs are broken, too: > "Of course. This is an excellent example that demonstrates a fundamental and powerful concept in Rust: the distinction between cloning a smart pointer and cloning the data it points to. [...]" Then I post the compiler's output: > "Ah, an excellent follow-up! You are absolutely right to post the compiler error. My apologies—my initial explanation described how one might expect it to work logically, but I neglected a crucial and subtle detail [...]" Aren't you also getting very tired of this behavior?
- the_mitsuhiko 1y ago> Aren't you also getting very tired of this behavior? The part that annoys me definitely is how confident they all sound. However the way I'm using them is with tool usage loops and so it usually runs into part 2 immediately and course corrects.
- bt1a 1y agoWell, they're usually told that they're some unicorn master of * languages, frameworks, skillsets, etc., so can you really fault them? :)
- renewiltord 1y agoHaha, I encountered the opposite of this when I did a destructive thing recently but first asked Gemini, then countered it saying it’s wrong and it insisted it was right. So the reality they encountered is probably that: it either is stubbornly wrong or overly obsequious with no ability to switch. My friend was a big fan of Gemini 2.5 Pro and I kept telling him it was garbage except for OCR and he nearly followed what it recommended. Haha, he’s never touching it again. Every other LLM changed its tune on pushback.
- ramon156 1y agoYou should check Twitter nowadays, people love this kind of response. Some even use it as an argument
- 1y ago
- tucnak 1y agoA bit off-topic, but every time I read some sophisticated Rust code involving macros, I cannot help but think that something went wrong at some point. The sheer complexity far outpaces that of C++, and even though I'm sure they would call C++ on undefined behaviour (and rightfully so) it seems less of it has to do with memory and thread-safety, and moreso with good old "C++ style" bloat: pleasing all, whilst pleasing none. Rust doesn't seem worthwhile to learn, as in a few years time C++ will get memory safety proper, and I could just use that. Maybe this is an improvement on templates and precompiler macros, but not really.
- junon 1y agoNone of this has to do with the complexity of macros. And no, sorry, the complexity of C++ templates far outweighs anything in Rust's macros. Templates are a turing complete extension of the type system. They are not macros or anything like it. Rust macro rules are token-to-token transformers. Nothing more. They're also sanitary, meaning they MUST form valid syntax and don't change the semantics in weird ways like C macros can. Proc-macros are self-standing crates with a special library type in the crate manifest indicating as such, and while they're not "sanitary" like macros rules, they're still just token to token transformers that happen to run Rust code. Both are useful, both have their place, and only proc macros have a slight developer experience annoyance with having to expand to find syntax errors (usually not a problem though).
- codedokode 1y agoProc macros are implemented in an unsafe way because they run arbitrary code during compilation and access any files. I do not like it. Also I think it would be better if they operated with reflection-like structures like functions, classes, method rather than tokens - they would be easier to write and read.
- junon 1y agoI agree in principle but also there's a lot of worth in having them do that in certain cases, and build scripts and the like already have that anyway. Achieving a perfect world where build tooling can only touch the things it really needs is less of a toolchain problem and more of an OS hardening issue. I'd argue that's outside the scope of the compiler and language teams.
- csomar 1y agoDerive Clone is not broken. It is basic. I’d say this is a good area for a dependency but not the core Rust functionality. Keep derive simple and stupid, so people can learn they can derive stuff themselves. It also avoids any surprises.
- josephg 1y agoI disagree. I think the current behaviour is surprising. The first time I ran into this problem, I spent half an hour trying to figure out what was wrong with my code - before eventually realising its a known problem in the language. What a waste of time. The language & compiler should be unsurprising. If you have language feature A, and language feature B, if you combine them you should get A+B in the most obvious way. There shouldn't be weird extra constraints & gotchas that you trip over.
- MangoToupe 1y agoI don't see it as a problem, personally. It's consistent behavior that I don't find surprising at all, perhaps because I internalized it so long ago. I can understand your frustration tho > in the most obvious way. What people find obvious is often hard to predict.
- saghm 1y agoThe main reason I'm not super fond of the way it currently works is that it can be a bit confusing in code reviews. I've joined several teams over the years working on Rust codebases around a year old where most of the team hadn't used Rust beforehand, with the idea that my Rust experience can help the team grow in their Rust knowledge and mature the codebase over time. I can recall numerous times when I've seen a trait like Debug or Clone manually implemented by someone newer to Rust where the implementation is identical to what would be generated by automatically deriving it, with a roughly equal split between times when they did actually need to manually implement it for the reasons described in this article and times when they totally could have derived the trait but didn't realize. If I can't look at a Clone implementation that just manually clones every field exactly the same way as deriving it would and immediately know whether it would be possible to derive it after over 10 years of Rust experience, I can't possibly expect someone with less than a year of Rust experience to do that, so my code review feedback ends up having to be a question about whether they tried to derive the trait or not (and to try it and keep it like that if it does work) rather than being able to let them know for sure that they can just derive the trait instead. I guess at a higher level, my issue with the way it currently works is that it's a bit ambiguous with respect to the intent of the developer. If it were possible to derive traits in the cases the article describes, seeing a manual implementation would be immediately clear that this was what the developer chose to write. The way it works right now means that I can't tell the difference between "I tried to derive this, but it didn't work, so I had to implement it manually as a fallback" and "I implemented this manually without trying to derive it first". It's a minor issue, but I think small things like this add up in the overall experience of how hard it is for someone to learn a language, and I'd argue that it's exactly the type of thing that Rust has benefited from caring about in the past. Rust has a notoriously sharp learning curve, and yet it's grown in popularity quite a lot over the past decade, and I don't think that would have been possible without the efforts of those paying attention to the smaller rough edges in the day-to-day experience of using the language.
- bloppe 1y agoI don't see how "the hard way" is a breaking change. Anybody got an example of something that works now but wouldn't work after relaxing that constraint?
- yuriks 1y agoIt relaxes the contract required for an existing type with derive(Clone) to implement Clone, which might allow types in existing code to be cloned where they couldn't before. This might matter if precluding those clones is important for the code, e.g. if there are safety invariants being maintained by Type<T> only being clonable if T is clone.
- bloppe 1y agoOk let's say there's existing code that requires that a type is not Clone. Then that type definitely would not have #[derive(Clone)] applied to it. So it would not be affected by the change. So it would not be broken. It's only a breaking change if code that previously worked stops working without changing the code.
- exfalso 1y agoEh. It's a stretch to call it "broken"
- jhugo 1y ago> we cannot just require all generic parameters to be Clone, as we cannot assume they are used in such a way that requires them to be cloned. No, this is backwards. We have to require all generic parameters are Clone, as we cannot assume that any are not used in a way that requires them to be Clone. > The reason this is the way it is is probably because Rust's type system wasn't powerful enough for this to be implemented back in the pre-1.0 days. Or it was just a simple oversight that got stabilized. The type system can't know whether you call `T::clone()` in a method somewhere.
- berkes 1y ago> The type system can't know whether you call `T::clone()` in a method somewhere. Why not?
- jhugo 1y agoTypes don't carry behavioral information about what the method does internally. Everything about a method is known from its signature. The compiler doesn't introspect the code inside the method and add additional hidden information to its signature (and it would be difficult to reason about a compiler that did).
- delta_p_delta_x 1y ago> Types don't carry behavioral information about what the method does internally. I was under the impression type inference meant that the implementation of a function directly determines the return type of a function, and therefore its signature and type.
- gg-plz 1y ago[dead]
- jhugo 1y agoWhile you can sometimes elide the return type (and what you describe only happens in closures — `|| { 0u32 }` is the same as `|| -> u32 { 0u32 }` — methods and free functions must always have an explicitly declared return type), that's not the same thing as being described above. For the existence of any invocation of `<T as Clone>::clone()` in the method body to be encoded in the method signature, we'd either need some wild new syntax, or the compiler would need to be able to encode hidden information into types beyond what is visible to the programmer, which would make it very hard to reason about its behavior.
- m3talsmith 1y agoJust looking at the examples, you can tell that they wouldn't compile: the other structs passed in don't derive the trait as well, nor implement it. It's really simple, not broken.
- kzrdude 1y agoThe only thing that needs change with derive(Clone) is to add an option to it so that you easily can customize the bounds. Explicitly.
- Surac 1y agoit seems i have a personal dislike for rust syntax. i think non of the code should compile because they are just ugly :)
- xupybd 1y agoAmazing site from someone so young
- xorvoid 1y agoAm I the only one who thinks this is perfectly fine? The requirements for derive Clone are clearly defined. As with much in Rust, the type signature drives things, rather than the function body (contrast with C++ generics). Occasionally, this results in perfectly reasonable code getting rejected. Such is the case with all static languages (by definition). But in the few cases that happen, the solutions are quite straightforward. So, I don’t feel like it’s justified to add more complication to the language to deal with a few small corner cases.
- bobbylarrybobby 1y agoIf you write code manually, you can forget to update it in the future. For instance, a manual implementation of PartialEq might become stale if you add new fields in the future. If you could automatically generate the implementation, and simply guide the macro to use non-default behavior (e.g. skip a field, or use a more complicated trait bound on a generic type) then you can have the advantages of generated code without the disadvantages. Seems worth trying for, IMO.
- PontingClarke 1y agoGreat write-up! It’s a solid reminder that #[derive(Clone)] can introduce subtle behavior in deeply nested or generic types. Automation is helpful—but shouldn't replace carefully reviewing your code's intent. Thanks for bringing attention to this!