We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Generics and Traits step 22 of 24
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 sealedis private. Nobody outside this crate can name the pathsealed::Sealed. -
trait Sealedinside it ispub. It has to be, or using it as a bound on apubtrait 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
Formatmethods, -
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
Formatin 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.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.