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.
Allowing more safe uses seems OK to me, but obviously expanding the functionality adds complexity, so there’s a trade off.
> in the most obvious way.
What people find obvious is often hard to predict.
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.
It’s not so terribly difficult to figure out what the expected behaviour is here. If I can write impl Clone in exactly the same way #[derive(Clone)] would do it, I should be able to just go ahead and use derive to do it. That seems pretty obvious to me.
Then again, I never had much respect for "obviousness" (as a concept, not in terms of code that is readable); the concept doesn't strike me as very useful except for papering over disagreement.
If addition in rust worked normally except when you add the number 4, the program panicked, that would be ridiculous. But why? Because it is inconvenient for one. And it’s not obvious. You need a more complex theory to model language like that.
The question with derive(Clone) is “what is the most straight forward theory” and if the actual language is more complex, we have to ask ourselves if the extra complexity is worth the cost.
If you spend 2 minutes thinking about it, as the blog post author said, if you want to implement clone for Foo<T>, it should be totally irrelevant if T implements clone or not. What matters is if the members of the struct implement clone. And that’s a completely independent question. Maybe T is clone and the struct members are not (eg the struct stores Cell<T>). Maybe the struct members are clone and T is not (eg if the struct stores Arc<T>). It’s a very strange, idiosyncratic theory that derive should have an invisible trait bound on T: Clone. It was also pretty surprising to the blog post author.
Sometimes complex theories pay their own rent. Like the borrow checker. Or fixed size integers. But if the theory is hidden, hard to learn, and doesn’t seem to help programmers, I’d call it a bug. Or a kludge. It’s certainly not an elegant programming language feature. This is why bringing up obviousness is relevant. Because good language features are either simple (and thus obvious) or they help me program enough to be worth the complexity they bring. This is neither.
This seems like a kludge to me. Derive should either impl clone when the children impl clone, or be universal, or univeral with optional custom trait bounds specified by the caller. (Eg derive(Clone where Foo<T>: Clone).
The advantage of a universal implementation is that you’d get more obvious compilation errors. “impl Clone of Mystruct<T>(T) requires T: Clone. Fix by adding where T: Clone to derive or manually implementing clone”. Simple. Straightforward. Obvious.