0

I am currently making my way through the Rust book and rustlings. try_from_into.rs asks us to implement TryFrom for a tuple, and array, and a slice. The array and slice versions fit nicely into an iter/map/collect pattern, e.g. as follows:

// Array implementation
impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;
    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
        match arr
            .iter()
            .map(|x| u8::try_from(*x))
            .collect::<Result<Vec<_>, _>>()
        {
            Ok(v) => Ok(Color {
                red: v[0],
                green: v[1],
                blue: v[2],
            }),
            _ => Err(IntoColorError::IntConversion),
        }
    }
}

Playground

Possibly this is not idiomatic either, so I'd appreciate any corrections.

However, the tuple implementation seems to leave me with two choices:

  1. Put the tuple (of 3 i16s) into an array and then use the map pattern above. This seems wasteful.

  2. Repeat myself by converting each value to u8, checking the result, and assigning to a local variable, e.g.

    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        let red = match u8::try_from(val) {
            Ok(v) => Ok(v),
            _ => return Err(IntoColorError::IntConversion),
        };
        let green = ...
        let blue = ...
        Ok(Color { red, green, blue })
    }
    

My first instinct is to put the match code into a helper and inline it if the language supported doing so, but Rust seems to have some barriers to that:

  • Private trait methods are not allowed, so I can't just add a helper inside the implementation. However, I lose any notion of what Self::Error is outside of the implementation.
  • We can't use generic parameters from outer functions, so neither can I create an inner function which uses Self::Error in the try_from definition, e.g. like
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        #[inline]
        fn i16_to_u8(val: i16) -> Result<u8, Self::Error> {
            match u8::try_from(val) {
                Ok(v) => Ok(v),
                _ => return Err(IntoColorError::IntConversion),
            }
        }
    
        let red = i16_to_u8(tuple.0)?;
        ...
    }
    

What are the Rust idioms for avoiding repeated code inside trait implementations, especially where a helper method seems like the obvious choice to someone coming from languages where these are commonplace?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
thisisrandy
  • 2,660
  • 2
  • 12
  • 25

2 Answers2

1

You could change the match block to a ? if you map the error into an IntoColorError. That will allow you to continue chaining methods so you can call try_into() to convert the Vec<u8> into [u8; 3], which can in turn be destructured into separate red, green, and blue variables.

impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;
    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
        let [red, green, blue]: [u8; 3] = arr
            .iter()
            .map(|x| u8::try_from(*x))
            .collect::<Result<Vec<_>, _>>()
            .map_err(|_| IntoColorError::IntConversion)?
            .try_into()
            .unwrap();
        Ok(Color{ red, green, blue })
    }
}

I'd probably break that up into two or three separate statements myself, but you get the idea.

If you apply the same error mapping idea to the tuple case you can get it fairly compact. It's still repetitive but the repetition isn't too bad thanks to ?:

impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        let (red, green, blue) = tuple;
        let red: u8 = red.try_into().map_err(|_| IntoColorError::IntConversion)?;
        let green: u8 = green.try_into().map_err(|_| IntoColorError::IntConversion)?;
        let blue: u8 = blue.try_into().map_err(|_| IntoColorError::IntConversion)?;
        Ok(Color{ red, green, blue })
    }
}

You can see both implementations on the Playground.

It is possible to eliminate the repetitive map_err calls. If you're on nightly then you could use a try block to capture the TryFromIntErrors and convert them to IntoColorErrors.

#![feature(try_blocks)]

impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        let result: Result<_, TryFromIntError> = try {
            let (red, green, blue) = tuple;
            let red: u8 = red.try_into()?;
            let green: u8 = green.try_into()?;
            let blue: u8 = blue.try_into()?;
            Color{ red, green, blue }
        };
        result.map_err(|_| IntoColorError::IntConversion)
    }
}

Playground

On stable you could achieve the same effect with an immediately-invoked function expression, or IIFE:

impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        (|| {
            let (red, green, blue) = tuple;
            let red: u8 = red.try_into()?;
            let green: u8 = green.try_into()?;
            let blue: u8 = blue.try_into()?;
            Ok(Color{ red, green, blue })
        })()
        .map_err(|_: TryFromIntError| IntoColorError::IntConversion)
    }
}

Playground


What are the Rust idioms for avoiding repeated code inside trait implementations, especially where a helper method seems like the obvious choice to someone coming from languages where these are commonplace?

Free functions. You can make helper methods outside the impl block.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • 2
    You could also implement [`From`](https://doc.rust-lang.org/std/num/struct.TryFromIntError.html) for `IntoColorError` to get rid of the `map_err`. – Aiden4 Oct 15 '21 at 23:14
  • I see. I definitely found myself missing the `try/catch` mechanism that we see elsewhere, so I'm glad to see one is in the works/can be effected in stable. – thisisrandy Oct 16 '21 at 03:36
1

You seem to have a little bit of a misconception of how associated types in traits work, because

However, I lose any notion of what Self::Error is outside of the implementation.

is not strictly true. When you leave the trait's impl block, you don't lose the notion of Self::Error, you lose the notion of Self. You can create a helper function outside the the impl that refers to the type:

fn i16_to_u8(n:i16) -> Result<u8, <Color as TryFrom<(i16,i16,i16)>>::Error>{
    n.try_into().map_err(|_| IntoColorError::IntConversion)
}

You can also make an associated function in a separate impl block for Color:

impl Color{
    fn i16_to_u8(n:i16) -> Result<u8, <Self as TryFrom<(i16,i16,i16)>>::Error>{
        n.try_into().map_err(|_| IntoColorError::IntConversion)
    }
}

The associated type for a trait exists in all scopes the trait is in. In many cases, you do need the fully qualified syntax, <Type as Trait>::Name so the compiler knows which implementation's type to use (this occurs for TryFrom because of a blanket impl over From).

You can also manually desugar the type alias yourself, but it does reduce maintainability:

fn i16_to_u8(n:i16) -> Result<u8, IntoColorError>{
    n.try_into().map_err(|_| IntoColorError::IntConversion)
}
Aiden4
  • 2,504
  • 1
  • 7
  • 24
  • This plays nicely off of John Kugelman's answer in that it explains how to implement free functions tied to `Color`'s error type. However, it is slightly wrong. For the free function, the compiler complains "ambiguous associated type ... help: use fully-qualified syntax: `::Error`", and something very similar for the method, which, by the way, should return a `Result`. I'm having a little trouble understanding quite what the help message is instructing me to do (what is `Trait` here?), if you wouldn't mind explaining. – thisisrandy Oct 16 '21 at 03:56
  • Also small nitpick, the function should convert to `u8`, not `i8`. – thisisrandy Oct 16 '21 at 03:56
  • @thisisrandy I addressed your comments (apparently I suck at proofreading), as well as added another solution I just thought of. – Aiden4 Oct 16 '21 at 04:18
  • much better! I see what the `as Trait` bit was hinting at now, though I don't think I would have gotten there without some help. If you could change the input param from `n:u16` to `n:i16` in the latter two examples, I'll accept this answer, since I think it goes more directly to my original difficulty, which was not seeing how to write helpers other than the desugared variety you present in your third example. SO insists on edits longer than 6 chars, so I can't do it myself. – thisisrandy Oct 16 '21 at 17:38
  • @thisisrand I fixed it, one of these days I'll figure out how to proofread properly. – Aiden4 Oct 16 '21 at 18:09
  • One more question, actually. You recommend implementing `From for IntoColorError` in the comments on John Kugelman's answer. I understand how to write the `impl` block and use it when chaining methods. However, in any of your examples, the function body that seems to work is `Ok(n.try_into()?)`, which just doesn't look right to me. Could you confirm that that's idiomatic or supply a better version if not? – thisisrandy Oct 16 '21 at 20:05
  • It turns out rustlings deals with error type conversion only a couple of exercises hence in [advanced_errs_1.rs](https://github.com/rust-lang/rustlings/blob/af91eb508ac659272398486f041b2a8364f9e385/exercises/advanced_errors/advanced_errs1.rs), and there's an example of `Ok(something()?)` at [line 40](https://github.com/rust-lang/rustlings/blob/af91eb508ac659272398486f041b2a8364f9e385/exercises/advanced_errors/advanced_errs1.rs#L40). Given that, I think I'm satisfied the pattern is idiomatic. – thisisrandy Oct 16 '21 at 20:35
  • 1
    @thisisrandy You can use `n.try_into().map_err(From::from)` if you don't like using `Ok(n.try_into()?)`. – Aiden4 Oct 16 '21 at 20:38