[Pre-RFC] Forbidding trait implementations for types

Summary:

Add a mechanism allowing types to list traits that are intentionally not implemented, and report attempts to implement any of these traits for the type as an error.

Motivation:

There are cases where the invariants of types can only be upheld through code inspection, when the type system could help instead. It is very easy to derive some traits on types that intentionally do not implement those traits, especially in development environments with multiple contributors. This RFC proposes a mechanism to inform the compiler of this fact, such that attempting to implement the trait turns into a compile-time error.

An example of this is a safe wrapper around unsafe pointer types. It may not be safe to clone this type, but the compiler will not complain if #[derive(Clone)] is added to the type declaration. This can be easy to overlook during code review because deriving Clone and other fundamental traits is such a common operation. The safety of the code would be better preserved if the compiler knew the intention of the original author of the safe wrapper and could report the conflict between the new change and the existing code.

Detailed Design:

Extend the existing implementation of impl !Trait for T to support any trait, not just Send or Sync. This feature is currently scope as an opt-out mechanism for a default implementation, but adding the ability to opt-out of manual implementations seems like a logical extension without any extra cognitive overhead.

This means that the following should be allowed:

struct NotCloneable;
impl !Clone for NotCloneable {}

And the following should report an error:

#[derive(Clone)]
struct NotCloneable;
impl !Clone for NotCloneable {}

This only affects traits that can be imported by the crate that declares the type. The !Trait bound can only contain such traits, and those traits cannot be implemented by downstream crates because both the traits and the type are not local to such crates. As such, this does not represent any more risk for causing breaking changes in the ecosystem than removing an existing trait implementation if such negative bounds are added.

Drawbacks:

  • The meaning of !Trait changes slightly from “ignore any default definition” to “this trait is not allowed to be implemented”.
  • The !Trait syntax is not stabilized yet (optin_builtin_traits feature), and tying this less-controversial feature to it makes it harder to stabilize.

Alternatives:

  • Add a new annotation to types like #[never_impl(Trait, AnotherTrait)] which means the same thing but avoids changing the meaning of an existing feature.
  • Add no new features; continue to rely on comments, code review, and compile-fail tests supported by the compiletest crate.

Unresolved questions:

None.

6 Likes

Another alternative is to use phantom types. If your struct contains a phantom data which is not Clone , you can’t derive it.

1 Like

This seems easy to implement as a custom annotation and a lint. Other than stability, is there a reason to prefer a built-in solution?

3 Likes

I prefer this option myself. In particular, if you are worried about somebody adding #[derive(Clone)] then it's good to put the thing that prevents the #[derive] where the derive would go.

If this were just about #[derive] then I would suggest that nothing is necessary. However, I think the proposed feature is more useful for non-derived explicit implementations. In particular, those implementations may appear very far away from the type definition, in any file in the crate.

You might also consider using the name #[deny_impl(...)] since "deny" is already used for similar things.

Previously and previously. This has many use cases (and implications) beyond the lint use you’re describing, because of the way it can be used to drive coherence.

3 Likes

I, too, agree that trait implementations for types should be forbidden.

(Sorry.)

3 Likes

If the goal is to prevent accidentally deriving a trait, we should only forbid (or even better warn about) deriving but not manual implementation. Forbidding manual implementation presumes the original author foresees every possible way the type can be used, which is usually not true. In practice this usually adds unnecessary restrictions resulting from poor decision made at the start. Rust has always been pretty opinionated but never dictative. It warns user, sometimes pretty strongly, against some use, but always assumes user knows better if they intentionally override. We should not deviate from this only to prevent accidents.

I’d be happy to hear arguments about type coherence though, which seems more limited to facilitate specialization and blanket implementation, rather than restriction intention of the type author.

I don’t see why it makes sense to privilege manual implementations over derived ones. If the implementation of the trait is desired, it should be required to remove the thing that is preventing it, rather than allow the code to contain contradictory information.

1 Like

While an interesting point that I hadn’t considered, this doesn’t prevent manual implementations.

It’s not clear to me that either of those RFCs actually cover the use case that I’m describing here. They appear to be concerned with specifying which traits are allowed to be implemented for types based on other traits that are implemented for those types. Is the idea that there could be a NeverClone trait which would forbid the Clone trait from being implemented?

I have not been able to come up with other reasons besides stability yet.

If I’m the author, and I forbid Clone being implemented, I expect the compiler to not let me implement Clone, no way no how.

I’ve written code in the past with a big comment attached basically saying “under no circumstances implement Clone for this, you can, but it’s hideously unsafe.” I fail to see how being able to enforce that is somehow a bad thing.

Besides which, there’s an easy fix: remove the proscription. If I’m in a position to proscribe the implementation of a trait, no one else is in a position to implement it anyway.

If the type is from another crate, then we won’t be able to remove the proscription easily. This will become relevant when we relax orphan rule a bit, and I think we should consider this beforehand.

I don’t agree. I mean, I see this as equivalent to (using Clone as an example again) wanting to be able to override a type’s implementation of Clone from another crate. Prohibiting Clone on a type is (very loosely) the same as implementing it such that every method just panics, except it’s more explicit and less dodgy.

If you really, really, desperately need to overrule the author’s (probably deliberate) choice to explicitly forbid the implementation of a trait, just wrap it in a newtype.

No, the idea is that you could implement !Clone for a type, which would mean, for example, that an impl for that type does not conflict with an impl for T: Clone (and also, of course, that you can’t implement Clone for that type).

1 Like

I doubt we will ever relax the orphan rules in the way you’re thinking (unless we just flat out give up on the idea).

In fact, the way I think we will resolve the problems the orphan rules present is to allow crate authors to more readily define blanket impls for their traits, that way cousin crates might be picked up by it. Negative impls are actually one of the ways we could do that because they enable you to declare that two traits are mutually exclusive, so blanket impls for each of those traits will not overlap.

For example, if Sequence and Map were mutually exclusive traits in the standard library (impl<T: Map> !Sequence for T and impl<T: Sequence> !Map for T), serde could provide generic serialization impls for every Sequence and Map types, and anyone who implemented them for their collection would have a serializable type without having to depend on serde directly.

4 Likes

Can you come up with more examples of traits that we might want to forbid in the name of safety? In general, I don’t think we need the ability to prevent the implementation of a trait, but it might be helpful to prevent the implementation of Clone if implementing it would cause an error.

Can you give an example of another language that has this feature? I can think of many mature languages that lack this feature and seem to get on fine without it.

I feel like this would be better implemented as an annotation, there’s no need to add more complexity to the language when it could just be implemented using existing techniques.

Something like a #[no_derive(Clone)] or #[no_impl(Clone)] (the name itself probably needs some bikeshedding) would make it fairly obvious to people what it means, plus using it as an annotation would mean it’s acting as more of a lint instead of a language feature.

I imagine a couple other use cases might be preventing someone from implementing traits that just don’t make sense for a particular type. For example, implementing Ord on a socket.

You could even add something like #[no_impl(...)] which would prevent someone from adding any of their own impls to your type. I can’t think of an example use case off the top of my head, but I’m sure someone might have a valid reason for something it.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.