Idea: Relaxation of the Orphan Rule

Right now, the orphan rule prevents any external type from being implemented by an external trait. But what if the orphan rules were relaxed to make sure that external types's implementations for traits cannot be publicly visible?

The Explanation

Say we have ExternType and ExternTrait, and we want a certain, useful function that ExternTrait provides. We can implement it for certain types like this:

impl ExternTrait for ExternType {
   fn extern_fn(val: ExternType) -> SomeType {
       // snip
   }
}

impl ExternTrait for ExternType {
   pub(super) fn extern_fn(val: ExternType) -> SomeType {
       // snip
   }
}

impl ExternTrait for ExternType {
   pub(crate) fn extern_fn(val: ExternType) -> SomeType {
       // snip
   }
}

// but not
// impl ExternTrait for ExternType {
//   pub fn extern_fn(val: ExternType) -> SomeType {
//       // snip
//   }
// }

This seems like a reasonable, crate-internal relaxation.

Visibility of trait implementations would itself be a very complex thing to add. Particularly, there is not necessarily a definite "call site" at which the implementation is used — the requirement for the local implementation to exist might only arise in a use of generic code from a downstream crate. There would need to be a new set of rules for defining which crate a use of a trait should be considered to be in.

It would be very useful if achieved, but it would need its own design and implementation.

3 Likes

As always, the hard part is the classic HashTable problem. If I have a HashTable<Foo> and pass it to you, we need to be using the same Hash impl, or the table will not work. Your proposal doesn't look like it solves that, because in my function I can be using my private Hash for Foo impl, and in your crate you can be using a different Hash for Foo impl.

Thus something needs to be in the signature for this to work -- and thus one often ends up at something not that different from a newtype, since the newtype in question is the thing in the signature that says which impl is in use, thanks to the orphan rule.

1 Like

I don’t think that necessarily follows: the visibility rules would say you can’t put HashTable<Foo> in your public API because it relies on a trait impl that’s not public. (I do think the visibility would belong on the impl though, not specific requirements.)

There was an RFC a little while ago that proposed adding scoped implementations of traits to get around the orphan rule (also including the possibility of one crate exporting its implementation for other crates to opt into). It was marked as postponed for not having the resources to implement, but it might interest people here to take a look: https://github.com/rust-lang/rfcs/pull/3634

The gist was that you could write

use impl ExternTrait for ExternType {
   fn extern_fn(val: ExternType) -> SomeType {
       // snip
   }
}

to indicate that you want to make a privately-scoped implementation, and put pub before the use to allow it to be exported for other crates to choose to opt in to your implementation.

The advantage to allowing for public trait impls is that I could write a crate for bevy_reflect_impls to add implementations of bevy's reflection traits to a bunch of third-party types in one location, and then all my other crates can import the same definitions from that crate and interoperate with each other.

1 Like

How would that work? HashMap<Foo> has no requirement for Foo to implement Hash or Eq.

1 Like

Oops, right. :frowning: Withdrawn!

@kpreid my current solution says that the impl is only crate-specific, and a downstream crate cannot directly use the impl. @scottmcm Maybe for that we could write a proc_macro_attribute that you can add to each trait to mark them as safe to relax the orphan rules? Eg... From, TryFrom?