Skip to content

← Generics and Traits step 22 of 24

Medium Primitives

Sealed traits: public to use, closed to implement

Sometimes you want a trait that everyone can use — write bounds against it, call its methods, store it in a Box<dyn _> — but that only you can implement. std does this. Big crates do this. The technique is called a sealed trait, and unlike most API-design tricks it is fully demonstrable in a single file, because the whole mechanism is module visibility.

mod sealed {
    pub trait Sealed {}
}

pub trait Format: sealed::Sealed {
    fn render(&self, v: i64) -> String;
    fn label(&self) -> &'static str { "fmt" }
}

Look carefully at the two visibilities, because the combination is the whole trick:

  • mod sealed is private. Nobody outside this crate can name the path sealed::Sealed.
  • trait Sealed inside it is pub. It has to be, or using it as a bound on a pub trait is an error.

Now Format is public and usable, but implementing it requires implementing sealed::Sealed first — and outsiders cannot even write that type’s name. The trait is sealed. Format itself must stay pub: the seal comes from the bound being unnameable, not from hiding the trait. Hide Format and callers could not write fn f<T: Format> either, which defeats the point.

Your task

pub trait Format: sealed::Sealed { ... }
pub struct Hex;  pub struct Binary;  pub struct Decimal;
pub fn render_all(inputs: Vec<(String, i64)>) -> Vec<String>

Each input is (tag, value) and renders as "{label}={rendered}":

tag output for 255 / 5 / 42
hex hex=0xff
bin bin=0b101
dec fmt=42
unknown ?=?

Read the dec row again. Decimal does not override label, so it gets the default "fmt". That is not a typo in the spec — it is there so you can see a default method actually being used.

Negative numbers go through the same formatters, so hex of -5 is hex=0xfffffffffffffffb: {:#x} on an i64 prints the two’s-complement bits, all 64 of them.

The error in the starter

The starter’s inner trait is not pub:

mod sealed {
    trait Sealed {}      // <- private
}
error[E0603]: trait `Sealed` is private
 --> src/main.rs:9:27
  |
9 | pub trait Format: sealed::Sealed {
  |                           ^^^^^^ private trait

E0603 is the general “you named a private item from outside its scope” error. Fixing it is a one-word change, and the point of the exercise is to understand why that word has to be pub while the module around it must not.

A note on a code you will see in older material. The neighbouring mistake — putting the private trait in the same module as the public one:

trait Sealed {}
pub trait Format: Sealed {}

used to be E0445, “private trait in public interface”. On this toolchain rustc --explain E0445 reports that the code is no longer emitted. The live diagnostic is the uncoded private_bounds lint:

warning: trait `Sealed` is more private than the item `Format`
   = note: `#[warn(private_bounds)]` on by default

It is warn-by-default, which under this course’s -D warnings gate means it still stops your build. If a tutorial tells you to expect E0445, it predates the change.

What sealing actually guarantees

It restricts implementation, not use. People conflate the two constantly. Outside code can still:

  • call Format methods,
  • write fn f<T: Format>(t: T),
  • store Box<dyn Format>,
  • match on your concrete types.

It just cannot add a new implementor. What you buy with that:

  • You can add a required method to Format in a minor release. Normally that is a breaking change, because every downstream implementor stops compiling. With a sealed trait there are no downstream implementors.
  • You can rely on the implementor list being exactly the one you wrote, which lets you make exhaustiveness arguments a compiler cannot.

The defaulted label is a smaller version of the same story: adding a method with a default is only possibly breaking even for an open trait (it can collide with an inherent method or another trait’s method at a call site), and for a sealed one it is safe.

In a single-crate program the seal has no teeth — you are inside the crate, so you could implement sealed::Sealed yourself. This problem therefore grades the structure plus the behaviour, and you should read the guarantee as one that only appears at a crate boundary.

The honest counterpoint

Sealing is exactly wrong for a plugin API. If the point of your trait is that users implement it — a Serializer, a Handler, a Backend — sealing it makes the crate useless for its intended purpose. Seal traits that describe a closed set of facts about your own types; leave open the ones that describe behaviour you want others to supply.

Remember the grade is compile + tests + clippy -D warnings.