Hi everyone,
I’ve been working on a preliminary idea to address some of the friction caused by the orphan rule, which I’ve tentatively named facets. I want to be upfront: this is very much a "half-baked" proposal. I have not conducted an exhaustive search of all previous RFCs or alternative designs, and I am fully aware that I might be reinventing the wheel or suggesting something that has already been dismissed for good reasons. Before I invest more time into formalizing this, I would value the community's feedback on two specific points:
- Is there already a proposal or an established design (even a rejected one) that covers this exact territory?
- Does the core mechanism seem like a direction worth investigating, or are there immediate blockers regarding complexity or safety that make this a non-starter?
Here is the current draft of the idea:
Facets and Controlled Structural Subtyping
Summary
This document introduces Facets, a variation on the newtype pattern that allows downstream crates to attach additional trait implementations to foreign types without violating Rust’s coherence rules.
Motivation
A common critique of proposals addressing the orphan rule is the risk of breaking global coherence. Designs that allow concurrent or scoped implementations often lead to ambiguous trait resolution or split-world scenarios where different parts of the same binary see different implementations for the exact same type. Facets explicitly reject this direction. Instead of weakening the orphan rule, Facets leverage Rust's existing nominal type system. This approach is a variation of the newtype pattern. Facets do not aim to make the orphan rule more permissive for a single type; they aim to make the creation and use of "almost-identical" nominal types ergonomic and zero-cost most of the time.
Core Design
Facet Types
A Facet is declared for an existing type Hello as follows:
facet foo for Hello;
At usage sites, the type Hello under facet foo is written as Hello\foo. The type Hello itself is considered the default facet. Facets based on the same base type are said to be related. Facets have the following properties:
Hello\foois a distinct nominal type from any other related facet.- All related facet types share the exact same memory representation.
- Every complex type involving a facet (e.g.,
Option<Hello\foo>) has the same memory representation as the type obtained by replacing the facet with any other related facet (e.g.,Option<Hello>). - No implicit conversions exist between related facet types.
Facet Inheritance
The declaration syntax above is a special case of the more general syntax:
facet foo: bar for Hello;
Where bar must be a pre-existing related facet. In this case, foo is a child of bar. If no parent is specified, it defaults to the default facet.
A facet baz is an ancestor of foo if there exists a chain of parent relationships from foo to baz.
When foo is a child of bar, it inherits all inherent and trait implementations defined on Hello\bar. Mechanically, the compiler behaves as if identical implementations were generated for Hello\foo.
Explicit Implementations for Facets
In the crate where it is declared, a facet foo can provide new implementations for traits. An implementation may define a brand-new trait or override an implementation already provided by an ancestor. This ensures that adding implementations to an ancestor in the future cannot break existing descendants.
We say that foo strictly inherits from an ancestor baz if foo does not override any implementations provided by baz.
Selective Trait Inheritance
A facet can explicitly reuse a specific trait implementation from another related facet:
impl Trait for Hello\foo: qux;
This mechanism may override an implementation of Trait inherited from an ancestor of foo. In that case, foo no longer strictly inherits from that ancestor, as it now has a distinct implementation.
Implementation Sharing
Two facets Hello\f1 and Hello\f2 share the implementation of a trait Trait if the compiler can prove they use the same implementation block through full or selective inheritance. This is the case if:
- One is a child of the other and does not override
Trait. - One selectively inherits
Traitfrom the other. - They both share the implementation with a common third facet (transitivity).
Conversion Semantics
The core principle of facet conversion is safety through ownership and immutability. Conversions are zero-cost at runtime but strictly governed by the compiler to prevent invariant violations.
1. Fundamental Types (Owned and References)
| Type | Conversion | Rationale |
|---|---|---|
Owned Value (Hello\f1) |
Universal | Safe. The owner has exclusive control; all invariants are explicitly chosen by them. |
Shared Reference (&Hello\f1) |
Universal | Safe. Immutability ensures the data cannot be modified in a way that breaks its invariants. |
Unique Reference (&mut Hello\f1) |
Forbidden | Strictly invariant. Prevents redefining implementations for mutating methods, which could break internal invariants. |
2. Wrappers parameterized by Facets
We call a type expression with a generic type parameter T a wrapper type. This can be a simple generic type such as Option<T> or a more complex type expression such as HashMap<usize, Vec<Option<T>>>. The type parameter T may appear in multiple positions, as in HashMap<T, T>.
The key question is to determine when it is safe to convert Wrapper<Hello\f1> into Wrapper<Hello\f2>.
Authors of generic types can opt into flexible casting by declaring, for each type parameter, which traits are required to preserve internal structure. For example:
struct HashMap<
#[structural(Hash, Eq)] K,
#[structural()] V
> { ... }
Here, Hash and Eq are structural traits for parameter K. Parameter V has no structural traits. A type parameter without a #[structural(...)] attribute is said to be unmarked.
| Type | Conversion | Rationale |
|---|---|---|
Owned unmarked wrapper types (Wrapper<Hello\f1>) |
Strict upcast | Safe. The result supports fewer operations than the input. This is a one-way operation because the original wrapper is consumed. |
Owned marked wrapper types (Wrapper<#[structural(...)] Hello\f1>) |
Implementation sharing of structural traits | Safe. Permitted if the compiler can prove that facets f1 and f2 share the same implementation for all structural traits. |
Shared references to unmarked wrapper types (&Wrapper<Hello\f1>) |
Strict upcast | Safe. Upcasting to &Wrapper<Hello\f2> is permitted under strict inheritance. Immutability prevents inserting incompatible facets. |
Shared references to marked wrapper types (&Wrapper<#[structural(...)] Hello\f1>) |
Implementation sharing of structural traits | Safe. Permitted if facets f1 and f2 share the same implementation for all structural traits. |
Unique references to any wrapper types (&mut Wrapper<Hello\f1>) |
Forbidden | Strictly invariant. Allowing facet changes through a mutable reference could violate the original owner’s invariants. |
3. Parallel Casting
It is valid to change multiple generic parameters simultaneously if each individual cast is valid. For example, casting HashMap<Key\f1, Value\g1> to HashMap<Key\f2, Value\g2> is permitted if both the K parameter cast and the V parameter cast satisfy their respective structural or strict inheritance requirements.