Skip to content

← Generics and Traits step 22 of 24

Medium Primitives

Sealed traits: public to use, closed to implement

Repair the visibility of a sealed formatting trait. Other crates should be able to use its methods and trait bounds, while this crate controls which types implement it.

mod sealed {
    pub trait Sealed {}
}

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

A supertrait is a trait required by another trait. Here, implementing Format also requires implementing sealed::Sealed. The private module prevents an outside crate from naming that required trait, so an outside type cannot implement Format through this API.

Preserve the formatting behavior

Keep Format, Hex, Binary, Decimal and render_all public. Each input to render_all is a (tag, value) pair. Return one string for each pair, preserving order. An empty input produces an empty output.

Tag Value Output
hex 255 hex=0xff
bin 5 bin=0b101
dec 42 fmt=42
HEX 255 ?=?
any other tag any value ?=?

Tags are case-sensitive and are not trimmed. Hex and Binary override label; Decimal inherits the default "fmt". Keep that default rather than changing the decimal prefix to dec.

Hexadecimal and binary rendering use {:#x} and {:#b} on an i64. They include the 0x or 0b prefix. For negative values these formatters show the 64-bit two's-complement representation, so hexadecimal -5 becomes hex=0xfffffffffffffffb. Decimal output uses a minus sign as usual.

Fix the visibility boundary

The starter declares the inner trait as private:

mod sealed {
    trait Sealed {}
}

Code in the parent module cannot name that private child-module item. References such as sealed::Sealed therefore produce E0603. Change the declaration to pub trait Sealed {} and keep the enclosing module private. The rest of the starter already implements the required formatting behavior.

The two visibility choices have separate effects:

  • The public inner trait can be named by code in this crate that can access its module, including the parent module where Format is defined.
  • The private module provides no public path to that trait for other crates.

Keep the seal out of public re-exports too. Making the module public, or publicly re-exporting its trait, would let an outside crate implement the seal for its own types and then implement Format.

A different example puts both traits in the same module:

trait Sealed {}
pub trait Format: Sealed {}

Here, name lookup is allowed, but the public trait has a less-visible bound. The private_bounds lint reports that mismatch and becomes an error under the warnings-as-errors gate. This differs from the starter's E0603. Older references may call this E0445; current diagnostics use the lint.

Separate use from implementation

A downstream crate can import Format, call its methods on the supplied types and write generic bounds such as T: Format. This particular trait is also dyn-compatible, so it supports Box<dyn Format> as used in the starter. Sealing alone does not make an arbitrary trait dyn-compatible.

The crate maintains the implementations of both the public trait and the seal. This control depends on the actual implementations: a blanket seal implementation for every type would defeat the intended restriction. Inside this crate, code with access to the seal can add implementations.

The grader compiles and runs a single program, so its output checks cannot by themselves demonstrate a downstream implementation failure. Preserve the requested module structure; the full visibility guarantee is best checked using a separate consuming crate.

Sealing does not make every API change compatible

Controlling implementations makes some changes easier: adding a required method does not force outside implementors to update, because this API has none. It can still break callers. For example, adding a generic method without a Self: Sized restriction can make Format unusable as a trait object. Adding methods can also introduce ambiguity with another trait's methods, whether the new method has a default or not.

Review the exposed API and its uses before deciding whether a change is compatible. Sealing is not a substitute for that review, and a trait's implementors do not become an enum that the compiler can match exhaustively.

Use an open trait when outside crates should provide implementations, such as a plugin extension point. Use sealing when controlling implementations is part of the API's design. Neither choice is universally preferable.

Passing requires compilation, the expected outputs and Clippy with warnings treated as errors.