The Rust Engineering Handbook / Chapter 20
Coherence, Orphan Rules, and Extension Patterns
Resolve forbidden and conflicting implementations with explicit ownership, wrappers, extension traits, sealing, and conversion boundaries.
The method is easy; owning its meaning is not
A settlement service receives WireAmount values from one dependency and must
write them through a reporting API owned by another. The integration crate
wants ordinary formatting at the boundary:
use std::fmt::{self, Display, Formatter};
use wire_types::WireAmount;
impl Display for WireAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{} cents", self.cents())
}
}
The body is harmless. The implementation is not. Display belongs to the
standard library, WireAmount belongs to wire_types, and this integration
crate owns neither side of the relationship. Rust rejects it with E0117.
Imagine that the rule were allowed. A second integration crate could quite
reasonably render the same amount as USD 12.34. A binary that acquired both
dependencies would then contain two meanings for WireAmount: Display.
Dependency order, import order, and proximity are poor ways to choose the
behavior of generic code. Adding an unrelated crate could change or invalidate
a call that had already been compiled and reviewed.
Coherence prevents that ambiguity by requiring at most one applicable trait implementation. The orphan rules assign which crate may establish a trait/type relationship; overlap checking ensures that the implementations it can write do not compete. The rejection is therefore useful design evidence: this crate may perform the formatting, but it may not claim that the formatting is the one global meaning of two foreign definitions.
Locality grants responsibility
For the ordinary, non-generic case, a crate can implement its local trait for a foreign type or a foreign trait for its local type. It controls at least one side, so other crates cannot independently create the same pairing. A foreign trait on a foreign type has no such owner here.
The full orphan rules also account for type parameters, fundamental types, and the order in which parameters are covered by local types. The Reference and the compiler diagnostic are the authority for those edges. “One side is local” is a good first model, not a substitute for the formal rule.
Locality is nominal. Moving the rejected implementation into a local module
does nothing. Re-exporting WireAmount changes its path, not its identity. A
type alias gives the same type another spelling:
type SettlementAmount = WireAmount; // still foreign
None of those operations accepts responsibility for a new type or contract. The repair must say what this integration actually owns.
Give settlement formatting a type of its own
Here the rendering is part of the settlement service’s reporting policy. A newtype gives that policy a local nominal home:
use std::fmt::{self, Display, Formatter};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SettlementAmount(WireAmount);
impl SettlementAmount {
pub const fn from_wire(amount: WireAmount) -> Self {
Self(amount)
}
pub const fn cents(self) -> u64 {
self.0.cents()
}
}
impl Display for SettlementAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let cents = self.0.cents();
write!(f, "USD {}.{:02}", cents / 100, cents % 100)
}
}
SettlementAmount is not camouflage for defeating the compiler. It states
that this service owns a monetary representation with a particular display
policy. Conversion at the boundary is visible, and future validation or
redaction rules have somewhere to live.
That ownership has a cost. Callers must construct the wrapper, decide when to borrow or unwrap it, and explicitly forward any inner behavior that belongs on the public surface. Those costs are often desirable: they expose the exact places where foreign data acquires local meaning.
Do not add Deref<Target = WireAmount> merely to recover every inner method.
Deref participates in method lookup and coercion; using it as inheritance can
erase the boundary the newtype was introduced to create. Likewise,
#[repr(transparent)] is relevant only when its documented representation
guarantees are required. It does not make the coherence argument stronger.
Add convenience without inventing identity
The same service also calculates a fee on a WireAmount. That operation is
useful at several call sites, but it does not make the value a new domain type.
A local extension trait can add the vocabulary without claiming a foreign
standard-trait relationship:
pub trait WireAmountExt {
fn checked_fee(&self, basis_points: u32) -> Option<u64>;
}
impl WireAmountExt for WireAmount {
fn checked_fee(&self, basis_points: u32) -> Option<u64> {
self.cents()
.checked_mul(u64::from(basis_points))?
.checked_div(10_000)
}
}
The trait is local, so this crate owns every implementation of it. Callers opt
in by importing WireAmountExt. That makes extension methods well suited to a
small, cohesive vocabulary used repeatedly with method syntax.
The opt-in is also a limitation. IDE discovery may be weaker until the trait
is in scope, and another imported trait may define the same method name. Fully
qualified syntax can disambiguate the call, but an ordinary function such as
checked_wire_fee(&amount, bps) is often clearer for one isolated operation.
Choose an extension trait because its vocabulary forms a useful local
capability, not because a method looks more fluent than a function.
The two repairs now express different ownership. SettlementAmount owns a
semantic identity and its standard formatting policy. WireAmountExt owns
convenient local operations on an unchanged foreign value. Neither is a
universal workaround for E0117.
Overlap spends space even when every definition is local
Owning a trait permits implementations; it does not permit ambiguity. Suppose
the reporting crate introduces ReportLabel and tries to provide both a
fallback and a special label:
trait ReportLabel {
fn label(&self) -> &'static str;
}
impl<T> ReportLabel for T {
fn label(&self) -> &'static str { "value" }
}
impl ReportLabel for SettlementAmount {
fn label(&self) -> &'static str { "settlement amount" }
}
The blanket implementation already includes SettlementAmount. Rust reports
E0119 rather than choosing the more specific body. Source order cannot make
trait selection coherent, and stable Rust does not use specialization to
rank these implementations.
The broad impl consumed the whole implementation space for ReportLabel.
Possible repairs make selection explicit: narrow the blanket bound so the sets
are provably disjoint, put special behavior on a wrapper, use separate traits,
or replace the trait with a function that performs an explicit match. Each
repair answers who selects the behavior. None merely persuades the compiler to
prefer one overlapping answer.
This is the danger in convenient blanket implementations. An impl for every
T satisfying a bound also covers types that have not been written yet. It can
prevent this crate from specializing later and can collide with downstream
implementations after either library evolves.
Conversions are implementations too
It is tempting to make the newtype accept anything convertible to
WireAmount:
// Broad, and probably broader than the domain contract.
impl<T: Into<WireAmount>> From<T> for SettlementAmount { /* ... */ }
This impl claims every present and future T: Into<WireAmount> relationship.
It also participates in the standard library’s conversion blanket impls. A
later dependency release or a new local conversion can create overlap that was
not visible when the convenience impl was published.
Prefer From<WireAmount> when conversion is infallible and semantically
unambiguous. Use a carefully scoped TryFrom<WireAmount> when the wrapper
enforces representability. Use a named constructor when conversion depends on
currency, configuration, rounding, or another policy callers should see.
TryFrom communicates failure, but it still occupies coherence space; its
scope deserves the same review as any other trait implementation.
Seal only a deliberately closed capability
Suppose the reporting library exposes storage modes and must exhaustively know which ones are durable. Allowing arbitrary downstream implementations would make that promise impossible. A private supertrait can close the set:
mod private {
pub trait Sealed {}
}
pub trait StorageMode: private::Sealed {
const DURABLE: bool;
}
pub struct Memory;
pub struct Durable;
impl private::Sealed for Memory {}
impl private::Sealed for Durable {}
impl StorageMode for Memory { const DURABLE: bool = false; }
impl StorageMode for Durable { const DURABLE: bool = true; }
Other crates can name StorageMode in bounds but cannot satisfy its private
prerequisite for their own types. The library gains a closed capability
vocabulary and more freedom to add required items. Users lose the ability to
extend that vocabulary.
That trade is an API promise, not a clever coherence technique. Seal a trait when consumers are meant to select among library-owned cases. Leave it implementable when third-party implementations are the product’s extension boundary. A trait advertised for plugins and secretly sealed contradicts itself.
Compatibility includes absent downstream code
Adding an impl can break code without removing any item. A downstream crate may already have a legal implementation involving its own local trait or type; a new blanket or conversion impl can make the combined program overlap. Public implementations therefore belong in compatibility review alongside functions and types.
Before publishing one, ask which future types it captures, which downstream local wrappers can participate, and whether a named operation would preserve more room. Sealing reduces the set of unknown implementers, but only by making the closed extension contract explicit. Chapter 44 develops the wider SemVer policy; the immediate lesson is that additive source text need not be additive in a dependency graph.
Negative impls and specialization-related features have narrow, unstable, or language-defined roles depending on the trait and feature. They are not a stable production escape from overlap. Prefer positive local types, disjoint traits, adapters, or explicit strategies whose selection can be read in the program.
Resolve the integration without hiding the owner
For the settlement service, retain fixtures that demonstrate E0117 for the
foreign Display implementation and E0119 for the blanket-plus-specific label
implementations. Then complete three legal paths:
- Make
SettlementAmountown the reporting identity and implementDisplayonly for that type. - Add checked fee calculation through
WireAmountExt, then compare it with a free function at call sites where the trait import is not otherwise useful. - Write a named adapter from
WireAmountinto the reporting API when neither a lasting identity nor reusable method vocabulary is needed.
Now change the requirements. Support two currencies whose formatting needs
configuration, and add a downstream crate that already wraps WireAmount.
Decide which conversions remain honest and which should become named
constructors. Sketch any blanket implementation you are considering and mark
the future types it captures before writing it. Do not use a type alias,
re-export, Deref, specialization, or negative reasoning to conceal the
ownership decision.
Review the implementation space
- Which crate owns the trait, the implementing type, or both?
- Does the implementation establish a global semantic relationship or only provide local adaptation?
- Would a newtype express identity, an extension trait express vocabulary, or a function express a one-off operation more honestly?
- What current and future types does each generic implementation cover?
- Can two legal dependency releases make previously separate impls overlap?
- Is conversion infallible and context-free enough to deserve
From? - Is a sealed implementation set genuinely part of the public contract?
- Does the stable design make behavior selection visible without relying on specialization?
Coherence answers who may attach a trait’s meaning to a type. Once that relationship is legal and unambiguous, the next choice is when a caller learns the concrete implementer. Chapter 21 follows that choice through static dispatch, closed enums, and trait objects.
Sources and version notes
- Rust Reference: trait implementation coherence
- Rust error E0117 and E0119
- Rust API Guidelines: coherence
- The E0117 and E0119 fixtures and the legal newtype, extension, and sealed patterns pass Rust 1.97.0 and the crate’s Rust 1.85.0 MSRV. No unstable feature is required or recommended.
Continue reading
Full table of contents