Pre-RFC: Sealed traits via `#[non_exhaustive]`

Sealed traits are very similar to non_exhaustive structs.

If you have:

#[non_exhaustive]
pub struct Foo {
    pub a: u32,
}

then

  • external users are not allowed to create instances of Foo
  • Foo may get fields added in the future without breaking users
  • it is equivalent to having a private field:
pub struct Foo {
    pub a: u32,
    private: (),
}

Sealed traits are very similar:

  • external users are not allowed to create types that implement the trait
  • the trait may get functions added without breaking users
  • it is equivalent to extending a private trait:
pub trait Foo : Private {
    ...
}

Therefore I propose that the #[non_exhaustive] attribute be applied to traits:

#[non_exhaustive]
pub trait Foo {
    fn foo(&self);
}

This would mean that:

  • external users can't impl the trait
  • new functions (without default implementations) may get added to the trait without breaking semver

An additional idea:

  • traits can have private items
  • traits can extend private traits
  • or even: traits can privately extend public traits

Externally all these cases would seem identical to #[non_exhaustive] and would be documented as such by rustdoc.

4 Likes

Are you aware of the Restrictions RFC:

https://rust-lang.github.io/rfcs/3323-restrictions.html

1 Like

The Restrictions RFC makes sense for sealing traits. But my main idea here is to use the same notation for sealed traits as for non-exhaustive structs. Perhaps non-exhaustive structs could also use the Restrictions notation? It's not part of the RFC.

Traits are basically compile-time structs. Implementing a trait is instantiating its struct.

1 Like

I don't think reusing the same notation is a good thing here, since you're overloading a term to mean something quite different. If anything, I'd call that attribute #[exhaustive], because the set of trait's implementations is exhaustively known and contained to the crate that defines it. That's why we can add new methods --- because we guarantee that we fully know and control all implementations. The set of methods is non-exhaustive, but that is already the case for ordinary traits, because I can always add methods with default implementation and sufficient trait bounds (e.g. Self: Sized).

Anyway, last time these things were discussed, this possibility was mentioned [1][2]. I don't think #[sealed] vs #[non_exhaustive] is a distinction which is worth the language complication, and in any case restrictions feel like a cleaner, more orthogonal solution which covers this case as well.

All structs are compile-time. It's the other way round: inherent impl on a struct behaves like a unique unnameable trait, with priority in method resolution.

1 Like

What I meant here is that instances of the struct defined by a trait (i.e. the vtables created by impls) are created at compile time. Traits = struct definitions, impls = struct instances.

1 Like

Non-exhaustive structs and enums are already stable, so there's no changing it. If you have a syntax question and/or suggestion for restrictions, I suggest you leave a comment on the tracking issue. I expect to get back around to the initial implementation in the near future (~in a week).