Skip to content

← Ground Rules: Values, Types, Control Flow step 11 of 24

Medium Primitives

From, Into, TryFrom: conversions that cannot lie

Convert each i64 to a u8, and say None when it does not fit.

pub fn to_bytes(values: &[i64]) -> Vec<Option<u8>>

[0, 255, 256, -1] becomes [Some(0), Some(255), None, None], which the harness prints as [0, 255, null, null].

The starter compiles and passes clippy. It returns [0, 255, 0, 255], which is what the previous item’s as does — and putting the two answers side by side is the lesson.

Three traits, one idea

trait From<T>     { fn from(value: T) -> Self; }
trait TryFrom<T>  { type Error; fn try_from(value: T) -> Result<Self, Self::Error>; }
  • From is for conversions that always succeed and never lose information. i64::from(u8) exists because every byte is an i64. u8::from(i64) does not exist, and cannot, because most i64s are not bytes.
  • Into is the same conversion read from the other end. You never implement it: there is a blanket impl saying that if U: From<T> then T: Into<U>. Write let x: i64 = byte.into(); when the target type is obvious from context, and i64::from(byte) when it is not.
  • TryFrom is for conversions that can fail, and it returns a Result carrying an error that explains why. u8::try_from(300i64) is Err(TryFromIntError(())).

The standard library implements these exhaustively across the integer types, and the presence or absence of an impl is a machine-checked statement about whether a conversion is lossless. That is a remarkable amount of documentation encoded as types.

Result and .ok()

try_from gives you Result<u8, TryFromIntError> — either Ok(value) or Err(why). This problem does not care why it failed, only that it did, so throw the error away:

u8::try_from(v).ok()      // Result<u8, E> -> Option<u8>

Result::ok maps Ok(x) to Some(x) and Err(_) to None. There is a matching Option::ok_or going the other way when you need to invent an error. Track 6 covers error handling properly; for now, .ok() is the one-word bridge.

The teaching line: “TryFrom by default, as on purpose”

Put the two implementations next to each other:

input as u8 u8::try_from(...).ok()
0 0 Some(0)
255 255 Some(255)
256 0 None
-1 255 None

The as column contains two answers that are indistinguishable from correct ones. Nothing in the type, the signature or the test output says that 256 became 0 because of a bug rather than because someone meant it.

Note the framing. It is not “never use as“. as is right for a deliberate truncation (the odometer in item 1.5), for a documented saturating float conversion, and in tight numeric code where the range is already established. The community genuinely argues about where the line sits. What is not contested is the default: if you have not thought about the failure case, you want the version that makes you.

Note on usize

usize is 8 bytes on the machines that grade this, so usize::try_from(i64) succeeds for every non-negative input here. It is still the right call, because the same source compiled for a 32-bit target would behave differently and try_from is the version that stays correct.

Loading visualization…