Skip to content

← Generics and Traits step 10 of 24

Medium Primitives

TryFrom, TryInto, and the type that cannot exist

From is for conversions that cannot fail. TryFrom is for the rest, and it is the idiomatic home for validation logic:

impl TryFrom<i64> for Percentage {
    type Error = PercentError;
    fn try_from(value: i64) -> Result<Self, Self::Error> { ... }
}

Putting the check here rather than in a free fn validate has a real payoff: because the associated Error type participates in From conversion, a TryFrom slots straight into the ? operator in any function whose error type can absorb yours.

Everything you learned about From/Into repeats one level up. Implement TryFrom; never implement TryInto. std contains

impl<T, U> TryInto<U> for T
where
    U: TryFrom<T>,
{ ... }

so .try_into() appears by itself. Once your TryFrom impl exists, writing impl TryInto<Percentage> for i64 yourself is:

error[E0119]: conflicting implementations of trait `TryInto<Percentage>`
              for type `i64`
   = note: conflicting implementation in crate `core`:
           - impl<T, U> TryInto<U> for T where U: TryFrom<T>;

Try it once, after your solution passes, so the shape of that error is familiar.

Your task

pub struct Percentage(pub u8);
impl TryFrom<i64> for Percentage { ... }
pub fn convert(vals: Vec<i64>) -> Vec<String>

A Percentage holds a u8 in 0..=100. For each input, convert produces:

input output
0, 50, 100 0% 50% 100%
101, 255 too large: 101, too large: 255
-1, 256, i64::MAX not a byte

So there are two distinct failures and the error type must tell them apart. Build them from u8::try_from(value): if that fails the value is not even a byte, and its error type is std::num::TryFromIntError. If it succeeds but exceeds 100, that is your own range failure.

convert must go through .try_into() and render the error with .to_string(), which means your error type needs Display.

The starter’s gate: Infallible

The starter declares type Error = Infallible and clamps instead of validating. Clippy refuses it, at default-on level:

error: infallible TryFrom impl; consider implementing From instead
  |
6 |     type Error = Infallible;
  |                  ---------- infallible error type

A TryFrom that never fails is a From wearing a costume — every caller now has to unwrap a Result that is always Ok. Either the conversion can fail, in which case say how, or it cannot, in which case implement From.

std::convert::Infallible is worth a moment on its own. It is an enum with no variants, so no value of that type can ever be constructed. The type system tracks impossibility, and two consequences follow that surprise people:

match e {}                       // legal: an empty match on an uninhabited type
size_of::<Result<u8, Infallible>>() == 1

The first compiles because the compiler can see there is no case to handle. The second says the error variant costs nothing — a Result whose error cannot exist is exactly as large as the success value. The starter uses the empty match; it is real Rust, not a trick.

One more lint in this neighbourhood

clippy::unnecessary_fallible_conversions is also default-on. It rejects i64::try_from(x) where x: u8, because that conversion cannot fail and i64::from(x) is exact. Reach for try_from when the conversion is genuinely narrowing, and from when it is widening.

Remember the grade is compile + tests + clippy -D warnings.