Converting to Option<T> generically

I have a struct that has Option-like semantics, that is:

pub struct Value {
   pub inner: Option<Inner>
}

pub enum Inner {
   String(String), 
   Int(i64), ...
}

I'd like to provide a convenient way for the user of my lib to convert Value to native Rust types.
Hence I defined a few conversions:

impl TryFrom<Inner> for i64 { ... }
impl TryFrom<Inner> for String { ... }

Now, how do I provide conversions to Option<T> for any T that can be converted to from Inner, without implementing all conversions for options by hand again?

Tried this:

impl<T> TryFrom<Value> for Option<T> where T: TryFrom<Inner>  { ... }

but that fails to compile:

error[E0119]: conflicting implementations of trait `std::convert::TryFrom<Value>` for type `std::option::Option<_>`
  --> src/lib.rs:55:1
   |
55 | impl<T> TryFrom<Value> for Option<T> where T: TryFrom<Inner>  {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T, U> TryFrom<U> for T
             where U: Into<T>;

Looks like core contains a much broader, generic definition of TryFrom for all the types, that conflicts with... everything? Can I somehow avoid that conflict?

1 Like

If you really need such a generic conversion, you can provide it as an inherent method:

impl Value {
    pub fn try_into<T: TryFrom<Inner>>(self) -> Result<Option<T>, T::Error> {
        // ...
    }
}

That won't be usable in contexts that take a generic U: TryFrom<Value>, though. If that's important you can satisfy coherence by implementing TryFrom<Value> for a local newtype Wrapper<T>(pub Option<T>) instead of for Option<T> directly.

[Edit: sorry for the typos in earlier versions of this post! I should've waited to wake up more before writing it.]

1 Like

Weirdly this works:

impl<T> TryFrom<Value> for Vec<T> where  T: TryFrom<Value>

It also works for my custom types:

enum MyOption<T> {
    None, Some(T)
}

impl<T> TryFrom<Value> for MyOption<T> where  T: TryFrom<Value>,

But changing to core Option immediately breaks it with E0119:

impl<T> TryFrom<Value> for Option<T> where  T: TryFrom<Value>

Why? What is so special about the core Option?

There's no (other) blanket impl<T> TryFrom<Value> for Vec<T>.

The problem is there is already:

impl TryFrom<Value> for Option<Value>

which would conflict with your impl for T = Value if you were to also implement:

impl TryFrom<Inner> for Value

The type checker is conservative and assumes this could happen.

3 Likes

Right, this is interesting—let's track down where the overlap is coming from in the case of Option and see why it doesn't occur for Vec. We're trying to make the following two blanket implementations coincide:

impl<X>    TryFrom<Value> for Option<X> where X: TryFrom<Inner> // local
impl<T, U> TryFrom<U>     for T         where U: Into<T>        // in std

This means we'll have to have U = Value and T = Option<X>, with the constraint that Value: Into<Option<X>>. This constraint is satisfied by picking X = Value due to

impl<X> From<X> for Option<X>             // in std
impl<Y, Z> Into<Z> for Y where Z: From<Y> // in std

So our two impls will overlap if Value: TryFrom<Inner>, as noted by @tczajka. Now this is a purely local condition (assuming Value and Inner are defined in the same crate where you're trying to write this), so I'm actually not sure why the compiler refuses. But you can see how the same reasoning fails to find a problem in the Vec case, because Vec has no analogue of impl<X> From<X> for Option<X> in std.

Anyway, the moral is that blanket implementations of the std::convert traits are tricky, and probably best avoided.

2 Likes

It would be nice if the compiler used where constraints to verify overlap between generic trait implementations - because in this case there is no overlap as Value type is fully controlled by my trait (and external crates won't be able to define conflicting conversions for it, so rejecting due to a potential overlap makes no sense).

And even it would be nicer if it allowed for specifying negative constraints for generic types, like where T != Value in this case, or if it allowed some overlap with rules to disambiguate like Scala has (Scala checks for conflicts on use site, not on definition site). In this case it seems my option conversion gets blocked by a totally uninteresting case - going from Value to Option<Value> which I'm ok to exclude.

However, you don't control std, and std could add conflicting impls, since it contains the traits themselves. Of course, std doesn't care about third-party crates' types, but this is a much more realistic problem for widely-used infrastructure crates like serde, which would like to implement their own traits for types in other, also widely-used infrastructure crates like chrono::Duration.

Not sure how you would "exclude" it? What do you expect the compiler to do? Disallow <Option<Value> as From<Value>>? Or disallow your impl when T == Option<Value>? That would be a bad surprise, since both impls exist, yet you can't use one of them. Who decides which one someone else is allowed to use? Certainly you shouldn't preclude others from using an already existing impl they may need. This kind of ambiguity creates all sorts of headache – the trait constraint system is not as trivial as it might seem at first.

Rust purposefully doesn't delay evaluation of bounds like that. C++ templates work in the same manner, and it's a huge pain, because you can write templated code that seems to compile just fine, except for when someone actually uses it. This is usually not too bad if you are using your own traits, but for library authors, who write traits subsequently used in many weird, interesting, and unforeseen ways, it is basically a completely unacceptable risk.

Or disallow your impl when T == Option<Value> ?

Yes, allow me to reduce the set of types my implementation applies to, in order to avoid the overlap.

The rule that I can define traits only for the types in my crate makes sense for non-generic traits, but currently it breaks down for generics, because I can't limit my blanket implementations to apply to the types in my crate only and conflicts with other crates are still possible (including the future ones). In case of a conflict, there isn't much I can do now.

And the problem you describe about code suddenly breaking exist today in Rust. Imagine the stdlib didn't have those blanket traits at the beginning - my crate would compile fine. Then someone later adds a blanket trait into stdlib, and voila - breaks my crate without me changing a single line of it. I don't think this property of suddenly breaking someone's code can be a good argument, when such property already exists in today's Rust.

I don't buy "we rejected your trait because someone else might implement a conflicting trait in the future" - because even with these rules this is still possible. Looks like making the average experience much worse to prevent an unlikely edge case (and still not perfectly).

Hence, we need to live with the possibility of trait conflicts happening. But then it would be good to provide more fine-grained ways to resolve them. There are many ways this could be achieved - e.g prioritization rules (e.g the most specific one wins), scoping or negative type constraints to name a few.

Also not sure why you mention C++ when I mentioned Scala. Scala does not use the same approach as C++ uses with templates. It has some strict prioritization rules and scoping which are typically enough to resolve conflicts on the use site. Someone provided a trait implementation in library A that conflicts with another implementation of the same trait in library B? Just let the user decide which one they want to use by importing only one of them (trait implementations, not whole libraries).

1 Like

It’s not really a problem because adding blanket implementations is considered a breaking change. An implementation impl<T> From<T> for Option<T> would also be in conflict with concrete implementations impl From<Foo> for Option<Foo> which would be allowed downstream (for a local type Foo) otherwise. You’d have to combine such an introduction of new blanket implementation with a semver major version bump, or in the case of std which doesn’t do breaking changes at all, introducing such an implementation later would be impossible unless it’s considered acceptable breakage (e.g. because it’s considered unlikely to produce problems with existing code and a crater run confirms this).

1 Like

Here's an issue for TryFrom specifically.

Explicitly opting out of a trait has been proposed before and may someday be part of coherence. Specialization is an accepted and at least somewhat implemented RFC which may also help (and is probably the same as your prioritization). I've wished for a where T: LocalToThisModOrCrate functionality myself, but you can usually work around the lack of that one with macros to implement for each concrete type.

macro_rules! impl_try_from_opt {
    ($($t:ty),+$(,)?) => {$(
        impl TryFrom<Value> for Option<$t> {
            type Error = <$t as TryFrom<Inner>>::Error;
            fn try_from(value: Value) -> Result<Self, Self::Error> {
                match value.inner {
                    Some(thing) => Ok(Some(TryFrom::try_from(thing)?)),
                    None => Ok(None),
                }
            }
        }
    )+}
}

impl_try_from_opt!(i64, String, /* ...*/ );
1 Like

Well, right, this is what I did in this case. Managed to get quite far with macros. :slight_smile:

Fortunately, there is no problem the other-way round. There exist a blanket conversion from T to Option<T> but fortunately there isn't Option<T> into T. Therefore, converting from my Value to Option with a simple blanket trait impl works just fine and no macros were needed.

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.