Rustlings, error handling for generic impl of try_from_into.rs

Hi all,

I've been starting rust for a couple of days, and really enjoy it. Rustlings is a well made tutorial and I do recommend it to any new learner.
Still, I've been struggling with this "try_from_into.rs" exercise, where the hint asked the following challenge :

Can you make the TryFrom implementations generic over many integer types?

I jumped into it, and implementd a (somehow) working solution. But I am not satisfied by the result of how I manage Errors.
Especially, I would like to use the "?" operator everywhere, but I can't find out how to make it work.
Here is the (partial) code :

#[derive(Debug, PartialEq)]
enum IntoColorError {
    // Incorrect length of slice
    BadLen,
    // Integer conversion error
    IntConversion,
}

impl<T: TryInto<u8>> TryFrom<(T, T, T)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (T, T, T)) -> Result<Self, Self::Error> {
        let (in_red, in_green, in_blue) = tuple;
        try_from_color_values(in_red, in_green, in_blue)
    }
}
fn try_from_color_values<T: TryInto<u8>>(
    in_red: T,
    in_green: T,
    in_blue: T,
) -> Result<Color, IntoColorError> {
    // Here, I would like to prevent the "or()" call and use "?" instead
    let blue = in_blue.try_into().or(Err(IntoColorError::IntConversion))?;
    let green = in_green.try_into().or(Err(IntoColorError::IntConversion))?;
    let red = in_red.try_into().or(Err(IntoColorError::IntConversion))?;
    Ok(Color { blue, green, red })
}

If I change into in_blue.try_into()?;, then I get compilation error :

error[E0277]: `?` couldn't convert the error to `IntoColorError`
let blue = in_blue.try_into()?;
   |                         ^ the trait `From<<T as std::convert::TryInto<u8>>::Error>` is not implemented for `IntoColorError`

Whatever I do, I can't implement the "From" trait correctly.

Do you have any ideas what is wrong or how I can make it work ?

A rambling portion where I explore the difficulties of trying to follow the trait advice

You can add the trait bound like it says:

fn try_from_color_values<T: TryInto<u8>>(
    in_red: T,
    in_green: T,
    in_blue: T,
) -> Result<Color, IntoColorError> 
where
    IntoColorError: From<T::Error>,
// aka:   IntoColorError: From<<T as TryInto<u8>>::Error>,
{
    let blue = in_blue.try_into()?;
    let green = in_green.try_into()?;
    let red = in_red.try_into()?;
    Ok(Color { blue, green, red })
}

And this code will now compile, but your implementation of TryInto for tuples fails. Because you've added a new bound on something it was relying on, you need to propagate that new requirement to the implementation:

impl<T: TryInto<u8>> TryFrom<(T, T, T)> for Color
where
    IntoColorError: From<T::Error>

And that will all compile. However, you actually haven't implemented any From<T::Error> for your IntoColorError type, so if you try to use it as-is, it won't actually work.

The error mentions TryFromIntError, that looks promising:

use std::num::TryFromIntError;
impl From<TryFromIntError> for IntoColorError {
    fn from(_: TryFromIntError) -> Self {
        IntoColorError::IntConversion
    }
}

And it works for i32, but not for u8 itself. At this point you could go track down what <u8 as TryInto<u8>>::Error is defined as and write another From implementation... but there's no way you can do that for everything that might implement TryInto<u8> individually (e.g. downstream code).

So you might try to get them all at once:

impl<T: TryInto<u8>> From<T::Error> for IntoColorError {
    fn from(_: TryFromIntError) -> Self {
        IntoColorError::IntConversion
    }
}

But this will tell you your implementation is too general.

error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
  --> src/main.rs:18:6
   |
18 | impl<T: TryInto<u8>> From<T::Error> for IntoColorError {
   |      ^ unconstrained type parameter

And let's pause here, because I think it's hinting at something important: You don't really care about what these other types are, or about actually doing a sensible conversion from them. If you did care, it would be hard to be that general, and I feel the exploration above hints at that. All you really want to do is throw those errors out and replace them with your own, and that's what the code that doesn't use ? is doing.

(I was going to explore some hacky ways around the roadblocks, but really nothing I came up with was actually any better or much shorter than what you're doing or using map_err or the like.)


In short, I think you're better off not adding more bounds here and trying to write enough implementations to satisfy them. Instead, if you step back a moment, this function is really just a helper for when you know you have three potential colors; it's not something you need to expose publicly like your TryFrom implementation itself.

So you might as well just use T::Error in the helper, and then in your implementation you can throw out those errors and replace them with your own. This helper function need not throw them out, or do any conversion at all:

fn try_from_color_values<T: TryInto<u8>>(in_red: T, in_green: T, in_blue: T)
-> 
    Result<Color, T::Error>
{
    let blue = in_blue.try_into()?;
    let green = in_green.try_into()?;
    let red = in_red.try_into()?;
    Ok(Color { blue, green, red })
}

// ...
        try_from_color_values(in_red, in_green, in_blue)
            .map_err(|_| IntoColorError::IntConversion)

And this way you also have no new bounds on your TryFrom implementation; it's as general as it reasonably can be.

Playground.

1 Like

Thanks for this detailed answer.
I still don't get why can't the map_err() logic be used in a From implementation for IntoColorError ?
Why does impl <T: "anything"> From<T> for IntoColorError not work/is not possible/allowed ?
Isn't it how map_err() and or() work ?

You mean, what's the problem with this:

impl<T: TryInto<u8>> From<T::Error> for IntoColorError {
    fn from(_: T::Error) -> Self {
        IntoColorError::IntConversion
    }
}

The short answer is that the language needs to be able to locate an implementation when it is given a (fully elaborated) trait and a potentially implementing type, so unconstrained type parameters are not allowed on implementations. In the above snippet, T is not constrained because there's no way to go from T::Error back to a specific T. (E.g. all the integer primitives use the same TryFromIntError.)

You could imagine it being possible by doing a more universal check, but that's not how the compiler works currently; the RFC indicates this might conflict with future specialization plans as well. [1]

Anyway, then you might try

// Heck it, convert them all!
impl<T> From<T> for IntoColorError {
    fn from(_: T) -> Self {
        IntoColorError::IntConversion
    }
}

And this would be legal, except there's already an existing blanket implementation which conflicts.

After that you start getting into the hackier workarounds like wrapping everything in your own dummy type, but at that point you have to convert everything anyway, so you might as well just convert to what you want.


map_err and or are inherent methods; once you know the type, the compiler can find the implementation. They introduce new generics on the function, not on the implementation.


  1. And there might be further difficulties, I didn't think about it too hard. ↩ī¸Ž

1 Like

Thanks for the explanations !

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.