Yet another half-baked idea for working around the orphan rule

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:

  1. Is there already a proposal or an established design (even a rejected one) that covers this exact territory?
  2. 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\foo is 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 Trait from 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.

2 Likes

I can't find it right now, but I'm pretty sure I've seen a proposal here on IRLO that is similar to this, but you would indicate which traits are being impl right in the type. I guess the syntax was something like type HashedMyType = MyType impl Hash = myimpl for a variant of MyType that uses a named impl of Hash, or something like that

Anyway both proposals are equivalent to newtype pattern, only with less boilerplate. Which is great news, since the downside of the newtype pattern is the huge amounts of boilerplate

However I think that boilerplate when defining the newtype is tolerable. What really sucks is wrapping/unwrapping types when using the types. That's what prevents me from using NonZero* types for example. So I think you need to have something to offer in this front.

Also, a small comment on syntax. I think that using punctuation in Type\foo isn't a good tradeoff. That's an uncommon feature and deserves a self-explanatory contextual keyword. Maybe reusing the pattern types syntax like Type is foo would work (if foo is an identifier this builds a faceted type, if it's a pattern with literals it forms pattern type. And maybe unify both concepts under a generalization). Or something like Type facet foo