Alternative / relaxed orphan rules to make working with foreign trait impls easier

While implementing a library, I found that I wanted to implement generic numeric ops for a trait I defined. That would consist of implementing Add, Sub, Mul, Div, etc. for each type that the given trait implements. In theory, I'd like to be able to do something like this:

impl<T: MyTrait> Add for T {...}

However, because of orphan rules, this definition isnt allowed, because Add isn't a crate local trait, and T isn't constrained to a crate local type. Instead, I have to wrap T in a newtype, and implement the trait for that:

impl<T: MyTrait> Add for MyType<T> {...}

Working around this quickly becomes ugly, and involves a lot of indirection, boilerplate, and just overall inconvenience that otherwise shouldn't be necessary; I need to impl Deref and DerefMut for the newtype, I need to forward all the trait methods that take ownership of the value manually, and I need to make sure I surround all new instances of the inner type with the newtype when constructing them, among other things. All this extra work just to prevent incoherence seems counterproductive and unnecessary.

To clarify: orphan rules as they currently are exist to prevent incoherence, where multiple crates may create impls between the same trait-type pairs. By requiring that either the implemented trait, or the implementee type are crate local, these rules prevent two implementations on the same trait-type pair ever existing between two disparate crates. As we can see however, that restriction can sometimes prove annoying to work around with regular type and trait usage.

I want to discuss a few ideas I've had as far as alternative solutions to orphan rules, which solve the same problem with less hassle from the developer.

Solution 1: Scoped Trait Impls

This is the most straightforward alternative solution; simply make impls of traits scoped, and require them to be in scope in order to be used. This is the way that rust disambiguates between dissimilar traits with the same name, so why not adopt it to impls of those traits as well?

Pros:

  • Completely frees all restrictions on trait implementation
  • Allows for the creation of "utility" libraries, which only provide useful impls between existing traits and types that the dev may choose to import

Cons:

  • Likely nontrivially increases the length of the module import section, more or less depending on the chosen syntax
  • Increases barrier to entry to language use, by making traits require additional work and understanding in order to be used
  • Requires a modification of the way the existing language is structured, requiring all code using it be updated to accommodate

Con-solutions:

  • The syntax can be adapted to work with existing functionality; by default, import all impls when directly importing a given trait. However, instead, the dev may choose to selectively import specific impls instead of the trait itself, which will leave out other impls defined by said trait. If the dev wants to also create impls for that trait in the same scope, they would need to reference the trait by its fully qualified name.

Solution 2: Allow impl Trait for T if T is constrained by at least one crate-local trait

This solution is less rigorous and well considered, as I'm not entirely sure if orphan rules can be violated under it or not. More discussion needs to be done.

Compared to the first solution, this one isn't nearly as dramatic; it solves the specific use case I demonstrated in my example, but it isn't much more freeing otherwise. To be fair, I would consider it a common use case.

Pros:

  • Does not require any modification of existing code, can be implemented in-place into existing rust
  • Does not significantly increase the complexity of understanding or implementing traits

Cons:

  • Covers significantly fewer use cases
  • Potentially not rigorous; may allow orphan rules to be violated
1 Like

Another pro to this would be that crates could impl Trait for themselves and not expose it as a semver guarantee. For example, I have an enum in a crate that, internally, I want to use as keys in a BTreeMap. However, I don't want to export more than Eq on the type to consumers (same with HashMap and Hash except that I want the consistent ordering of BTreeMap).

Something like this would work for me:

#[derive(PartialEq, Eq)] // visible to all consumers
#[derive(pub(in super), Hash, PartialOrd, Ord)] // only visible within `super`
enum Blah { Variant1, Variant2 }

I'd say that this would disallow impl Hash for Blah anywhere within the crate, but I'm not sure how easy it would be to enforce that.

I would also expect that this is allowed:

pub fn visible_outside_crate() -> impl Hash {
    Blah::Variant1
}

but this only allows Hash-needing method access within the pub scope:

fn partially_useful() -> HashMap<Blah, ()> {
    HashMap::new()
}

Coherence breaks down in the face of:

// crate C
trait C { /* … */ }

// crate A
trait A { /* … */ }
impl<T: A> C for T { /* … */ }
impl A for (i8, i8) { /* … */ }

// crate B
trait B { /* … */ }
impl<T: B> C for T { /* … */ }
impl B for (i8, i8) { /* … */ }

// crate U
let use: Box<dyn C> = Box::new((0, 0)); // what vtable gets used here?

mmm, I see. I prefer solution 1 anyway.

could you go more in depth on that syntax you suggested?

I suppose the desugared version is more interesting here with:

pub impl trait A {}

Given that the default is pub (something an edition could change if wanted?), all uses are likely to use the pub(…) syntax, so pub(in self) would be "private". The derive() syntax is just a way to indicate that pub needs specified for the generated impl blocks.

With the upcoming work on sealed traits, there might be a way to base this on that. Because even if it's a trait-local crate, it might still have a trait-local impl. But if it only applies for sealed traits, there's no external impls to worry about.

There are still, I used to think that this would be fine even without sealing, as long as you put the blanket impl in the first version the trait appears in you're just restricting downstream from implementing both your crate-local trait and the trait you blanket impl, but there's the potential for issues with two levels of blanket impls

use std::ops::{Mul, Add};

trait Foo { ... }

// already allowed to blanket impl a local trait
impl<T: Mul> Foo for T { ... }

// new ability to blanket impl a foreign trait based on a local trait (sealed, or otherwise)
impl<T: Foo> Add for T { ... }

Now for some type Bar: Mul + Add there's two potentially applicable impls of Add.

I think this could still work if there were some restrictions on not being able to do blanket impls of the trait like that, you either get to implement your trait for foreign types or implement foreign traits for types that implement your trait, never both.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.