Feature Name: sealed_traits
- Start Date: 2022-06-11
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
Trait definitions can have a #[sealed]
attribute placed on them. This stops users in other crates
from implementing the trait.
Motivation
It is common to want a trait that can only be implemented for certain types. This is true in both the standard library and many crates. It is currently possible to create a trait that is public but unnameable. This prevents users in other crates from implementing the trait, but also from using it.
mod private {
pub trait Sealed {} // Users in other crates cannot name this trait.
impl Sealed for u8 {}
}
pub trait Actual: private::Sealed {}
impl Actual for u8 {}
This approach has drawbacks. The documentation generated by rustdoc
is not very good (through no
fault of its own), as private::Sealed
is sometimes where the implementation actually takes place.
If private::Sealed
is used as a generic bound, Actual
is not necessary, resulting in the trait
being completely undocumented: only the trait name is visible in documentation. Because two traits
are necessary in some cases, it is possible for the user to implement private::Sealed
but not
Actual
. This is a logical bug, but one the compiler lets happen.
Having sealed traits as part of Rust itself solves these issues and opens new use cases. A trait
that is sealed would appear alongside other items in documentation, but could contain an annotation
(similar to the one for #[non_exhaustive]
) telling the user that they cannot implement it
themselves. Sealed traits eliminates the need to have “helper” traits to restrict implementation:
the compiler enforces it instead. This can lead to improved error messages, reducing frustration.
Explanation
A new attribute, #[sealed]
, is introduced. This attribute can only be placed on trait definitions.
A “sealed trait” is any trait that has the #[sealed]
attribute placed on its definition.
Assume the following trait definition and implementation.
#[sealed]
pub trait Foo {}
struct Bar;
impl Foo for Bar {}
When Bar
is in same crate as Foo
, the #[sealed]
attribute has no effect. If Bar
is in a
different crate, a compiler error will be emitted. For example,
error: cannot implement sealed trait
--> $DIR/sealed-traits.rs:25:1
|
LL | impl SealedExternalTrait for Local {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
::: $DIR/sealed-trait.rs:3:1
|
LL | #[sealed]
| --------- trait sealed here
|
= note: sealed traits cannot be implemented outside the crate they are defined in
It is a breaking change to add #[sealed]
to a trait that already exists. This is because users in
other crates may have implemented it. It is not a breaking change to remove #[sealed]
.
Implementation
A new compiler pass for coherence is added that checks if a trait with #[sealed]
is being
implemented outside of the crate it is defined in. The compiler will emit an error if this is the
case. This new pass is similar to the existing pass for unsafe
traits and #[non_exhaustive]
enums.
Whether a trait is sealed has no effect on anything other than restricting implementations. This
means that applying #[sealed]
to a trait does not affect coherence.
Drawbacks
This introduces a built-in attribute for behavior that can be partially accomplished in library code.
Rationale and alternatives
The existing workaround is widely used and has its drawbacks (mentioned above). One alternative is to introduce native syntax. This would require a (possibly contextual) keyword to be added.
Prior art
-
Sealed traits are currently simulated by using a public trait in a private module.
mod private { pub trait Sealed {} impl Sealed for u8 {} }
-
The
sealed
crate is a procedural macro that generates a public trait in a private module.
Unresolved questions
None so far.
Future possibilities
- The
#[sealed]
attribute could accept a path, as in#[sealed(module)]
or#[sealed(in module)]
, restricting the ability to implement the trait to only that module. - Trait items could be extended to have full support for visibility, just as structs do. This would eliminate the need for continued use of the workaround for when private methods are wanted.
- As this proposal does not affect coherence, it is possible that a future RFC could have
#[sealed]
affect coherence in some way. This would likely require different or additional syntax. This is because removing#[sealed]
is not a breaking change, but it would be if it affected coherence.