Trait bounds limitations (generic TryFrom<T> impl). Hypothetical future impl triggers conflicting implementations error?

I was trying to implement a TryFrom trait for a custom type and came up on this interesting issue:

My new type wraps a NonZeroU16. So I implemented TryFrom<u16> first, no issues there:

use std::num::NonZeroU16;

pub struct MyStruct(NonZeroU16);

impl TryFrom<u16> for MyStruct {
    type Error = <NonZeroU16 as TryFrom<u16>>::Error;

    fn try_from(value: u16) -> Result<Self, Self::Error> {

Since try_from is fallible anyway wouldn't it be nice to have it generic over all possible "numeric" types? Let's try that and replace the imlp with a generic one:

impl<T> TryFrom<T> for MyStruct
    NonZeroU16: TryFrom<T>,
    type Error = <NonZeroU16 as TryFrom<T>>::Error;

    fn try_from(value: T) -> Result<Self, Self::Error> {

Looks like we were a little too generic with our impl, we get an error:

error[E0119]: conflicting implementations of trait `std::convert::TryFrom<_>` for type `MyStruct`
  --> src/
28 | impl<T> TryFrom<T> for MyStruct
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: conflicting implementation in crate `core`:
           - impl<T, U> std::convert::TryFrom<U> for T
             where U: std::convert::Into<T>;

Looks like if some type U implements std::convert::Into<T> then it gets TryFrom<U> "for free", which conflicts with our implementation. Ideally we would exclude such types by adjusting the where clause with something like:

    NonZeroU16: TryFrom<T>,
    T: !Into<NonZeroU16>,

... but that is not a valid syntax today.
Side question: Are there any plans/RFCs to include such a thing?

We will need to restrict the type T another way.
Surely we should be able to at least extend our generic implementation to some predefined set of types. Lets create our own "marker trait" for that and try (starting with our original set of 1 containing only u16):

trait NZConvSource {}
impl NZConvSource for u16 {}

impl<T: NZConvSource> TryFrom<T> for MyStruct
    NonZeroU16: TryFrom<T>,
    type Error = <NonZeroU16 as TryFrom<T>>::Error;

    fn try_from(value: T) -> Result<Self, Self::Error> {

But a big surprise, unfortunately we still get the same error:

error[E0119]: conflicting implementations of trait `std::convert::TryFrom<_>` for type `MyStruct`
  --> src/
17 | impl<T: NZConvSource> TryFrom<T> for MyStruct
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: conflicting implementation in crate `core`:
           - impl<T, U> std::convert::TryFrom<U> for T
             where U: std::convert::Into<T>;

Weird. Shouldn't our new impl be equivalent to the original non-generic one? The only type implementing our new bounding trait NZConvSource is u16 and surely u16 doesn't implement Into<NonZeroU16>.

What's even weirder: If we delete the impl NZConvSource for u16 {} line we still get the same error ... for an empty set of types ?

What's the issue? Am I understanding trait bounds wrong? Is there some other current limitation on trait implementations I'm not aware of? Because the error message doesn't seem to be very helpful in this case.

Not in any form that's realistically going to be available in the foreseeable future. Negative reasoning is hard, this is not an accident or an omission. You should avoid relying on negative reasoning and re-formulate your impls instead.

Yes you are. Coherence can't just take into account the current set of implementing types. It must be conservative and assume that all potentially possible imps may appear one day. Since downstream crates can implement NZConvSource (however unlikely it is) for their own types, the compiler has to act as if they did, otherwise 3rd-party code could break your own code (and there's nothing either you or the downstream code's author could do about it at that point).


Rust may (I’d say it’s fairly likely) in the future support sealed traits, which disallow foreign impls and as such should also have more lenient orphan rules.

But doesn't this mean that any foreign trait that also has some kind of (even very restricted) generic implementation can't be extended to you own types (because they could possibly conflict)?

... Sorry, just trying to wrap my head around what is currently possible...

Also. Using the same logic:

What's preventing the hypothetical future std:: / core:: implementation from providing the TryFrom implementation for my type themselves and creating the conflict (with my existing non generic impl) that way?

Could you provide a link to current coherence rules regarding this? The Book kind of glaces over these things and rules mentioned there still don't prevent conflicts (crate owning the type or the trait means still 2 possible conflicting impls)

How come? Isn't NZConvSource a private trait?

My idea how conflict resolution between trait implementations could work (seems like currently not the case):

  1. Trait on a type can be implemented by the crate originating the trait or the type.
  2. The crate declaring the trait could specify the mode of conflict resolution
    • in case of conflict use the impl from traits originating crate (the default)
    • opt-out and let the trait implementors override the trait implementation for their types
  3. Conflicts within the crate are forbidden (compiler error)

That doesn't matter. Private items can be made public without being considered a breaking change, so visibility must not be respected by coherence checking.

Usually, what's not in the Book will be in the Reference. I'm not sure whether it answers your question 100%.

This seems to be all the Reference has on this subject:

  • orphan rules seem fine
  • The core of the problem seem to be in the formulation: the implementations can be instantiated with the same type. That's not exactly a formal spec :slightly_frowning_face:.

If I understand you correctly one should not read it as:

There currently exist a type for which conflicting instantiations could be made

but instead read it as:

There exist a type that could be implemented in some outside crate, regardless of the current visibility rules in this crate that would lead to two conflicting instantiations. "some outside crate" currently meaning the other one of the two possible crates permitted by orphan rules.

Am I right?
If so, that seems way too restrictive for nobody's benefit. What is it protecting against? Some future potentially incompatible change in the new version of the outside crate while I simultaneously change the visibility of my own type/trait?

I suspect that was not the intention and the real reason will be much more technical...

On a different note:
What is the reason behind trying to prevent conflicts across crates (instead of just having some simple conflict resolution rules like in my post above)?

There's a plan for generalized negative impls, that let you explicitly opt out of a non-auto trait.

// This would be a semver guarantee that the trait won't be implemented
impl !From<u16> for NonZeroU16;
impl !Into<NonZeroU16> for u16;

I recommend reading the hackmd and/or this initiative summary for more context. The part about coherence taking negative implementations into consideration is the with_negative_coherence feature (no separate tracking issue).

    NonZeroU16: TryFrom<T>,
    T: !Into<NonZeroU16>,
// Note that this would mean `T` has *explicitly opted out*
// of `Into<NonZeroU16>`, not that it just happens to not
// implement it currently

It changes the overlap checks but not the orphan rule checks. I suspect it wouldn't actually help your OP (but as far as I know the new disjointness check is unimplemented so there's no way to play around with it).

The practical TL;DR is that currently, one blanket implementation makes any other blanket implementations impossible, forcing you to use more concrete implementations. Negative reasoning like "the blanket impl doesn't apply to this type" is done for local types only.

There is a good citation for the orphan rules.

I don't have a good, single citation for the overlap rules and local negative reasoning. Privacy doesn't matter though. I want to say the other pertinent rule is that where clauses currently don't matter except for negative reasoning (local types). But again I can't back that up without taking the time to revisit a lot of discussions.

(I too would love to see a specification of the current rules if someone does have a better citation.)

The parts of coherence that aren't about preventing unsoundness are about

  • preventing dependency hell or the possibility of incompatible crates
  • ensuring that programmers have the freedom to do some reasonable set of things without creating a major breaking change

It will always be reasonable for someone to add a trait implementation for their own type, for example. So it should be impossible for me adding a new trait implementation to my type to break something else. If non-local negative reasoning was allowed, any additional trait implementation to my own type would be a breaking change.

Exactly what scenario you're asking about is unclear to me. But I doubt privacy will every matter. Instead it would be some opt-in thing like the aforementioned explicitly sealed traits.

This is not a new area of discussion; there's weeks of reading material around if you want to pursue it. Here's one starting spot with a ton of linked RFCs and other discussions you could follow up on. A lot of it is orphan rule specific but there are also discussions about negative reasoning, exclusive traits, etc.

Relevant RFCs I found so far:

Other resources:
Chalk - Coherence

I agree with that. But the rules I suggested above:

would in my opinion:

  • preserve compatibility with existing codebase
  • preserve the required one unambiguos single source for each types particular trait impl, while only the two affected creates need to be considered to make the decision (source crate for the trait and for the type)
  • keep the power over the traits impl on it's author/originator, while:
  • allowing him to let outside implementors override if he thinks it's desirable
  • let us more freely implement outside traits without breaking the intention of their original author

Sure, the rules would need to be extended i.e. for specialization (does more specialized trait impl have priority over more generic originating one?), but having "some" rules for conflict resolution across crates (whatever they would be) seems like a much better idea then trying to avoid them at all cost and significantly restricting the expressive power of the language.

Maybe after more in-depth study of the resources I will change my mind, but currently that's my opinion.

What you're describing is (a very constrained form of) specialization, i.e., allowing multiple implementations so long as a single implementation has formal precedence.

I don't see how it would apply to the OP though, if I understand correctly. By dint of blanket implementations you end up with

impl<T> TryFrom<T> for MyStruct where NonZeroU16: TryFrom<T>
// =>
impl<T> TryInto<MyStruct> for T where NonZeroU16: TryFrom<T>

And T isn't your type...

1 Like

No-no. There is no TryInto. Read the error again. I'm prevented from implementing TryFrom by some future hypothetical implementation of the Into trait (that currently doesn't exist) that would lead to TryFrom being auto-implemented for my type. At least that is my explanation so far.

Specifically it call's for this blanket implementation from core:::

impl<T, U> TryFrom<U> for T
    U: Into<T>,
    type Error = Infallible;

    fn try_from(value: U) -> Result<Self, Self::Error> {

so to conflict the core would need to implement something like:

impl Into<MyStruct> for u16 {

or simillar (while MyStruct is my private type actualy it's not in my example but same applies:, but visibility is not being considered here either)

Another failure mode that H2CO3 suggested is that core:: could implement my private trait NZConvSource for some type that already has TryFrom implemented but thinking about it further I can't see how that would apply since that would only instantiate TryFrom trait for MyStruct which wouldn't conflict.

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.