Why does implementing `TryFrom<MyType>` for u64 *not* violate the orphan rule?

Playground link

I've created a generic UInt<N> type and to my surprise, I'm able to impl<const N: usize> TryFrom<UInt<N>> for u64. Neither TryFrom nor u64 are defined in my crate (they're both in core).

Why is this legal?

It's because UInt is in your crate, so no other crate can implement this. The orphan rules specify this in the 2nd rule:

Given impl<P1..=Pn> Trait<T1..=Tn> for T0, an impl is valid only if at least one of the following is true:

  • Trait is a local trait
  • All of
    • At least one of the types T0..=Tn must be a local type. Let Ti be the first such type.
    • No uncovered type parameters P1..=Pn may appear in T0..Ti (excluding Ti)

It's the "At least one of the types.." part.

1 Like

Ah I see. I was referencing the traits section in the Rust book, which implies the orphan rule is more restrictive than it is. Maybe the italicized orphan rule in the book should link to the rule in the language spec?

1 Like

The orphan rules are there to enforce coherence, which is that there mustn’t be multiple overlapping instances of the same trait for the same type with the same generic parameters, even across crates. Additionally, they are designed so that conflicts always appear locally, not later down the line – so two crates that both depend on a common trait, define some types, write some impls, if those two crates each compile independently, then they ought to also compile successfully when combined together!

From these two principles, the orphan rules are to be understood; they are not arbitrarily restrictive, and beyond these principles, they are aiming to avoid unnecessary additional limitations.

The simple case of a parameter-free trait BarTrait, and a type FooStruct, to implement BarTrait: FooStruct can happen either in the crate that defines BarTrait or the one that defines FooStruct. Crates that do neither are not specific enough, there can be two or more of such crates, independently importing both the trait and the type as a dependency and defining the implementation; then those two (or more) crates together would create an overlapping/conflicting impl.

Why isn’t there potential for conflict by allowing both the BarTrait’s defining crate and the FooStruct defining crate to write the impl though!? It’s because there’s another catch: To be able to write the impl BarTrait for FooStruct, the implementing crate must know of both types. Either the crate defining BarTrait depends on the one defining FooStruct, in which case only the BarTrait-defining crate can ultimately write that impl BarTrait for FooStruct, or vice-versa, then the FooStruct-defining crate can write that impl. It’s never both that can do it because Rust crates cannot have cyclic dependencies.

Now, with the case of three components involved… a single-argument trait BarTrait and two structs FooStruct and QuxStruct, and an impl BarTrait<QuxStruct> for FooStruct. The situation now is quite similar to above, but there’s three things (two types and one trait) at play and only one of the crates that defines each of them can know of them all simultaneously without circular dependency. Hence, it’s okay to allow, say, the QuxStruct-defining crate to write the impl BarTrait<QuxStruct> for FooStruct, assuming it depends on the other crates that define FooStruct and BarTrait.

The orphan rules as quoted above however are a bit more complex than the simple “at least one type, or the trait, must be local”. This is because they also account for blanket implementations; something not really important for your case, but an interesting design choice nonetheless: With blanket (i.e. generic) implementations taken into account, we now realize that either of the Self-type or the type argument(s), or both, could be type parameters:

An impl<S, T> BarTrait<S> for T is quite clearly only implementable where BarTrait can be defined. Interestingly enough, such an implementation does conflict with something like impl BarTrait<QuxStruct> for FooStruct that we discussed above, even though that one could have been written in a different crate downstream that depends on BarTrait’s crate. This is not a contradiction to the principles though, because now, writing such an impl downstream would always, immediately overlap, because the crate must have depended on the BarTrait’s crate already. It’s not like it’s an independent crate that only results in overlap later if somehow combined with BarTrait’s crate further downstream.

This does however illustrate how blanket impls are breaking changes if they are introduced later. Introducing impl<S, T> BarTrait<S> for T breaks a possibly pre-existing impl BarTrait<QuxStruct> for FooStruct down the line, so BarTrait’s create mustn’t introduce it in a semver-minor version.

There are two other blanket impls that overlap with impl BarTrait<QuxStruct> for FooStruct, namely

  • impl<S> BarTrait<S> for FooStruct
  • impl<T> BarTrait<QuxStruct> for T

again, let’s still assume FooStruct’s crate and QuxStruct’s crate both depend on BarTrait’s crate. If we were to simply say “well, FooStruct is local to it’s crate, so that create may write the first impl” and “well, QuxStruct is local to it’s crate, so that create may write the second impl” then the above two implementations could exist in independent crates. (Neither of FooStruct’s crate or QuxStruct’s crate needs to depend on the other in order to be able to write this impl.) But the two impls would be overlapping, both covering the case BarTrait<QuxStruct> for FooStruct.

This is why the orphan rules introduce an asymmetry, arbitrarily. It rules out the second impl above, and only allows the first one. It does so by assigning an order to the types in an impl like BarTrait<S> for T, and saying that the position of T, the Self-type, comes before the position of the first generic argument S, then the position of the next argument, and so on. And following this order it says that generic type parameters must not come before the (first) local type. (Feel free to check back to the formally written rules quoted by @jumpnbrownweasel above now. The only thing still missing, and I will skip that detail here entirely, is the distinction between bare type parameters, and certain special types within which type parameters still are considered “uncovered”.) Above, in the two impls I listed, the second impl has T come before QuxStruct, so QuxStruct’s create mustn’t write the second impl – the impl<T> BarTrait<QuxStruct> for T– (and thus ultimately, in the crate hierarchy at hand, no crate can write such an impl at all, unfortunately… the orphan rules are conservatively restrictive in this context, fully ensuring no overlap, but not ensurign full coverage of all impl possibilities).

This order and rule (can be shown to) indeed successfully prevent(s) overlaps as in the example above, in all cases of generic impls, for any number of arguments.

The full consequences of semver constraints (avoiding breaking changes) with all kinds of blanket implementations is a bit complex, and ultimately not seldomly, generic trait implementations turn out to be breaking changes to add in situations where you don’t really expect it. That’s a separate topic, but this effect underlines the rules are already really permissive, perhaps too much so. Note that we could hover consider even more lax rules:

See, in a case like impl SomeTrait for SomeGenericType<AnotherType>, there, too, there are 3 things – 2 types and 1 trait – at hand, yet we do not allow a crate that just defines the type AnotherType locally to write such an impl based on just that. The motivation here is no longer coherence, but reasonable semver constraints. We want an addition of an implementation impl<T> SomeTrait for SomeGenericType<T> later down the line not to be a breaking change in the crate that defines SomeGenericType!

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.