Search code examples
genericsrusttype-conversiontraitsidioms

Implementing Into when possible and using TryInto otherwise


I have a struct:

pub struct Point2D<T>(pub T, pub T);

I want to be able to:

  • (A) Turn a Point2D<T> Into a (T, T) with .into
  • (B) Turn a Point2D<A> with .tryInto into a (B, B) if A implements TryInto<B>

I can implement (A):

impl<T> std::convert::Into<(T, T)> for Point2D<T> {
    fn into(self) -> (T, T) {
        (self.0, self.1)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn into() {
        let a: Point2D<u32> = Point2D(42, 42);
        let a_into :(u32, u32) = a.into();
        let b: (u32, u32) = (42, 42);
        assert_eq!(a_into, b);
    }
}

And I can implement (B):

#[derive(Debug)]
pub enum PointIntoError<T> {
    ErrorInX(T),
    ErrorInY(T),
}

impl<FromType, ToType> TryInto<(ToType, ToType)> for Point2D<FromType>
    where FromType: TryInto<ToType>,
          FromType: Copy {
    type Error = PointIntoError<FromType>;

    fn try_into(self) -> Result<(ToType, ToType), Self::Error> {
        let x_ = self.0.try_into();
        let y_ = self.0.try_into();

        match x_ {
            Ok(x) => {
                match y_ {
                    Ok(y) => Ok((x, y)),
                    _ => Result::Err(PointIntoError::ErrorInY(self.1))
                }
            }
            _ => Result::Err(PointIntoError::ErrorInX(self.0))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn try_into() {
        let a: Point2D<u32> = Point2D(400, 400);
        let a_into :(u16, u16) = a.try_into().expect("Could not convert to tuple of u16");
        let b: (u16, u16) = (400, 400);
        assert_eq!(a_into, b);
    }

    #[test]
    #[should_panic]
    fn try_into_fail() {
        let a: Point2D<u32> = Point2D(400, 400);
        let a_into :(u8, u8) = a.try_into().expect("Could not convert to tuple of u8");
    }
}

And both tests pass when run individually. However, when I try to compile both impls, I get:

error[E0119]: conflicting implementations of trait `std::convert::TryInto<(_, _)>` for type `Point2D<_>`:
  --> src/lib.rs:31:1
   |
31 | / impl<FromType, ToType> TryInto<(ToType, ToType)> for Point2D<FromType>
32 | |     where FromType: TryInto<ToType>,
33 | |           FromType: Copy {
34 | |     type Error = PointIntoError<FromType>;
...  |
49 | |     }
50 | | }
   | |_^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T, U> std::convert::TryInto<U> for T
             where U: TryFrom<T>;

error: aborting due to previous error

For more information about this error, try `rustc --explain E0119`.

The error is not particularly helpful, but I'm guessing there's some sort of default implementation for TryInto for structs that implement Into, and hence I cannot define both at the same time.

What is the most idiomatic way to support both Into conversion when it makes sense and TryInto conversion? I suppose I could always .tryInto and .expect when I know for sure that the types match but it doesn't seem neat.


Solution

  • I'm guessing there's some sort of default implementation for TryInto for structs that implement Into, and hence I cannot define both at the same time.

    That is correct, more specifically there's a blanket implementation of TryFrom for any structure implementing Into (which is blanket-implemented for any structure implementing From): https://doc.rust-lang.org/std/convert/trait.TryFrom.html#impl-TryFrom%3CU%3E

    What is the most idiomatic way to support both Into conversion when it makes sense and TryInto conversion? I suppose I could always .tryInto and .expect when I know for sure that the types match but it doesn't seem neat.

    I think the most idiomatic would be to convert your Into implementation to a normal method e.g. .into_tuple(), this is a reasonably common pattern, for instance slice::into_vec or String::into_boxed_str.