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

This breaks down when the trait implementation names the implementing type somewhere else.

For example imagine you create a facet foo for i32, then i32/foo inherits PartialEq<i32> and Eq. However PartialEq<i32/foo> is not implemented for i32/foo, hence the Eq implementation is an error. In this case the issue is that the PartialEq implementation uses the i32 type again in the trait generic parameters.

Some usage examples would really help me understand the proposal. Especially, it is not clear to me whether this addresses the main place where I tend to trip over the orphan rule:

// in a binary crate; `unix_path` and `rustix` are external libraries
use unix_path::Path;    // a concrete type
use rustix::path::Arg;  // a trait

// rustix::fs::open's signature is something like
//    fn open<T: Arg>(path: T) -> Result<rustix::fd::OwnedFd, rustix::Error>;
// 
// neither library provides this impl and I'm not allowed to do it:
impl Arg for Path { /* ... */ }

// which means I have to have wrappers like this instead:
pub(crate) fn open_fd(path: &Path)
    -> Result<rustix::fd::OwnedFd, MyError> {
    rustix::fs::open(
        path.as_unix_str().as_bytes(), // <-- blech
        O_RDONLY, M_IGNORED
    )
        .map_err(Into::into)
}

What I want out of improvements to the orphan rule is a way to pass unix_path::Path objects directly to rustix::fs::open (and other APIs that take arguments with trait bound rustix::path::Arg) with no extra ceremony at each callsite. Does your proposal give us that?

(See my post in this older thread for more detail.)

Hi everyone, and thank you for these insightful technical points.

This is precisely the friction point Facets aim to eliminate. Consider NonNull<T>. Today, if you have a newtype, you are forced to map/unwrap the container. With Facets, you can cast the nested type directly:

  1. Strict Upcasting: If T\f1 is a descendant of T\f2, then NonNull<T\f1> can be cast to NonNull<T\f2> as a no-op.
  2. Structural Invariance: By marking NonNull with struct NonNull<#[structural()] T>, the author allows casting between any facets of T. Since NonNull does not rely on any specific trait of T to maintain its "not null" invariant, this is perfectly safe and removes the need for any manual wrapping.

I have no "religious" preference regarding the syntax. If the semantics of the design are deemed viable, we will have plenty of time to "bikeshed" the notation. The priority now is to determine if the underlying logic holds water.

I apologize, as my initial presentation was indeed unclear on this point. In my model, facet inheritance involves a systematic substitution of Self.

Specifically, impl PartialEq<Self> for i32 on the base type is inherited as impl PartialEq<Self> for i32\foo on the facet. This ensures that Eq remains valid. If an implementation explicitly names the base type (e.g., i32) instead of Self, the facet inherits it as-is.

Because a facet is a distinct nominal type, you "own" it and can implement the external trait for it:

// 1. Define a local facet for the external type
facet WithArg for unix_path::Path;

// 2. Implement the external trait for your local facet (Legal!)
impl rustix::path::Arg for Path\WithArg { /* ... */ }

fn main() {
    let path = get_path(); // Assume this is a unix_path::Path
    let path = path as unix_path::Path\WithArg; // no-op
    rustix::fs::open(path, O_RDONLY, M_IGNORED);
}

I suspect that requiring path as unix_path::Path\WithArg still constitutes "ceremony" from your perspective. While implicit coercion could be envisioned to bridge this gap, I believe it would introduce too much "magic" and obscure the transition between nominal identities. I opted for an explicit cast to maintain predictability and avoid "spooky action at a distance," which I consider a necessary trade-off for a system that strictly preserves global coherence.

Yeah. Don't get me wrong, this would be an improvement over what I have to write now: I wouldn't have to repeat a chain of non-obvious conversions at every callsite! But it is still obscuring the actual logic at this level with conversions, and it's still something that has to be done at every callsite.

But if you could make this work, that would be good enough for me

// once in the crate
facet WithArg for unix_path::Path;
impl rustix::path::Arg for Path\WithArg { /* ... */ }

// anywhere the `facet WithArg` declaration is visible
fn main() {
    rustix::fs::open(get_path().into(), O_RDONLY, M_IGNORED);
}

It's still something I have to do at each callsite, but it's much shorter and -- because it doesn't involve naming Path\WithArg -- much less distracting from the actual logic.

That still sounds problematic because it introduces a distinction that doesn't currently exists, plus it doesn't fix all breakage.

FYI there was a discussion of a very similar feature Struct Alias where some of these points were raised.

Hi SkiFire13,

Thank you for pointing me to that discussion. After reviewing it and exploring some edge cases further, I’ve identified several scenarios where this model is indeed not viable.

I agree that the design cannot work within Rust's fundamental constraints without introducing unacceptable complexity or breaking encapsulation. I am stepping back from this proposal.

Thank you for your guidance and for helping me identify these fatal flaws early on.

1 Like

This maybe?: [Pre-RFC] Scoped `impl Trait for Type` - #20 by Tamschi

Probably not exactly what you were referring to since that specifies anonymous module-addressed implementations (which is helpful for subsetting generic implementations in that RFC). It's in principle very similar to what was proposed here, though, at least in terms of type system interactions, and discusses afaik all the edge cases that have come up over time.

The main formal issue that proposals like this run into is that currently you can make unsafe assumptions about bounds-linked implementations, which makes solutions like this either:

a) unsound,
b) make implementation of traits a SemVer hazard in general or
c) introduce more friction (require unsafe) to reuse of global implementations in those combinations.

I went with[1] c) in my RFC (unsafe use ::{impl Trait for Type}; to allow it consumer-side, and/or a boolean attribute impl-side), but considering how much of a niche case that likely is in practice, I still think it would be sensible to flip the default for whether a global impl Trait for Type can safely be combined with non-global impls elsewhere as part of an edition.

(There are also "soft" issues related to friction at crate APIs, though I think a proposal that distinguishes types and is always explicit, like this one here, should have fewer of those.)


  1. I still haven't gotten around to actually revising the RFC with this fix properly, but it's outlines fairly unambiguously in the GitHub comments at least :melting_face: ↩︎

1 Like