Skip to main content

sealed

Attribute Macro sealed 

Source
#[sealed]
Expand description

Seals a trait so that only the listed types can implement it, and every listed type must implement it.

#[sealed(Circle, Square)] // error: Square does not implement Shape
trait Shape {}

struct Circle;
impl Shape for Circle {}

struct Square;

impl Shape for i32 {} // error: i32 is not permitted

§Entries

An entry is a type, written plainly or as a path, as in #[sealed(Square, shapes::Circle)], and that is all of it where neither the trait nor the type is generic. Cases where one or both of them are generic are presented in the next sections.

§A generic trait

A generic trait has to be told which of its instantiations the entry implements. Entry: Trait<..> says which:

struct Plain;

#[sealed(Plain: Store<i32>)]
trait Store<T> {}

impl Store<i32> for Plain {}

One instantiation can be enough, but the type may implement the trait at every one of them. for<..> declares a parameter for the entry to instantiate with:

struct Plain;

// every `Store<T>`, not just one
#[sealed(for<T> Plain: Store<T>)]
trait Store<T> {}

impl<T> Store<T> for Plain {}

A parameter the binder declares can carry bounds:

struct Plain;

#[sealed(for<T: Debug> Plain: Store<T>)]
trait Store<T> {}

impl<T: Debug> Store<T> for Plain {}

The bounds are part of what is sealed: Plain is permitted Store<T> only where T: Debug, so an impl<T> Store<T> for Plain covering every T is refused.

Lifetimes, types and const parameters can be declared together, lifetimes first (as in for<'a, T: Clone, const N: usize>), and each is written exactly as it would be on an impl.

§A generic type

The parameters a for<..> declares serve the type just as well, which is how a trait with no parameters of its own seals a generic type:

struct Ref<'a, T>(&'a T);

#[sealed(
    for<'a, T: Shape> Ref<'a, T>,
    Square,
    Circle,
)]
trait Shape {}

impl<'a, T: Shape> Shape for Ref<'a, T> {}

§A generic type under a generic trait

One binder covers both, and a name it declares may stand in the type and the instantiation alike.

struct Boxed<U>(U);

// every `Boxed<U>`, each at the matching `Store<U>`
#[sealed(for<U> Boxed<U>: Store<U>)]
trait Store<T> {}

impl<U> Store<U> for Boxed<U> {}

The binder’s names are its own, so where they go is what counts, not what they are called: for<U, V> Pair<U, V>: Store<V, U> seals Pair<U, V> at Store<V, U>, swapped. And the instantiation’s arguments are ordinary types, so a parameter can sit inside a larger one rather than be the argument itself: for<T> Keyed<T>: Store<Vec<T>>.

§as Name

Names the entry. Only enumerate reads it: a variant is otherwise named after the type it holds, and two entries whose names come out the same would be one variant twice, which is refused. as Name gives one of them a name of its own. This happens in two ways.

Different types whose last segment matches. Here the name settles which is which:

mod a { pub struct Foo; }
mod b { pub struct Foo; }

#[enumerate]
// without `as`, both would take the last segment `Foo`
#[sealed(a::Foo as Left, b::Foo as Right)]
trait Shape {}

impl Shape for a::Foo {}
impl Shape for b::Foo {}

let _ = AnyShape::Left(a::Foo); // see enumerate

The same type listed twice, which is how one type reaches the enum at more than one instantiation. There the name is not a nicety but required, since both entries would otherwise be the Plain variant:

struct Plain;
struct Boxed<T>(pub T);

#[enumerate]
#[sealed(Plain: Store<i32>, Plain as PlainF64: Store<f64>, for<T> Boxed<T>: Store<T>)]
trait Store<T> {}

impl Store<i32> for Plain {}
impl Store<f64> for Plain {}
impl<T> Store<T> for Boxed<T> {}

// the one type reaching two different enum instantiations
assert!(matches!(Plain.into_enum(), AnyStore::<i32>::Plain(_)));
assert!(matches!(Plain.into_enum(), AnyStore::<f64>::PlainF64(_)));

The name settles the variant only. The two entries must also pin different arguments, and some entry (Boxed<T> here) has to mention T. The enum is generic over the parameters its variants use, not over the trait’s, since an enum may not declare one no variant uses. Drop Boxed<T> and nothing is left to be generic over: both entries become variants of one plain AnyStore, so Plain converts into it two ways and into_enum has two answers. enumerate refuses that. Keeping the enum generic is what puts the two entries in AnyStore<i32> and AnyStore<f64>, one Plain apiece.

§All of it at once

A binder, the type, a name and the instantiation it implements, in that order:

struct Ref<'a, T>(&'a T);

#[sealed(
    for<'a, T> Ref<'a, T> as RefStore: Store<i32>
)]
trait Store<T> {}

impl<'a, T> Store<i32> for Ref<'a, T> {}

§The list is checked in both directions

Every entry is checked, which is why a trait that declares type or const parameters needs them supplied for each of its entries, and the instantiation is what supplies them: Plain: Store<i32> pins them, for<T> Boxed<T>: Store<T> passes on what its binder declared. An entry without one is refused, since nothing could then tell whether it implements the trait at all. A trait declaring none asks nothing, which is why #[sealed(Square, Circle)] above needs no annotation.

A lifetime is never asked for, and not merely because inference usually copes. A type cannot implement the same trait at two different lifetimes: two such impls overlap, and coherence rejects them, so there is never more than one candidate to disambiguate. #[sealed(Plain)] under trait Foo<'a> is therefore accepted and checked: an entry implementing no Foo at all is still caught.

§The seal is as precise as the list

The marker carries the same type and const parameters the trait does, so Plain: Store<i32> permits Plain to implement Store<i32> and nothing else: an unlisted impl Store<f64> for Plain is rejected:

struct Plain;

#[sealed(Plain: Store<i32>)]
trait Store<T> {}

impl Store<i32> for Plain {}
impl Store<f64> for Plain {} // error: not permitted to implement `Store` here

An entry whose binder supplies them instead, like for<T> Boxed<T>: Store<T>, permits every instantiation, which is what the binder says. Lifetimes are not on the marker, for the reason above: they could never tell two entries apart.

§What the seal is worth

The marker trait is private to the module the attribute is written in, and carries a supertrait private one level deeper. Naming the marker is therefore not enough to satisfy it: the only place both can be implemented is inside the generated module, which nothing but this macro writes. Code sitting directly beside the sealed trait cannot opt a type in, which a single level of privacy would have allowed.

The cost is that permitted types must be nameable from that module, so they have to live at module level. A type declared inside a function body cannot be sealed, because no module nested in a function can refer to it.