084 — From and Into Traits
Tutorial
The Problem
Implement Rust's From and TryFrom traits for type conversions: bidirectional temperature conversion between Celsius and Fahrenheit, parsing a Color from &str with TryFrom, and validating a raw user record into a typed User. Compare with OCaml's explicit named conversion functions.
🎯 Learning Outcomes
From<A> for B and gain Into<B> for A automaticallyTryFrom<&str> when conversion can fail, returning Result<Self, Self::Error>map_err and ? in TryFrom implementationsimpl<T, U: From<T>> Into<T> for U.into() at call sites for ergonomic type conversionCode Example
//! 084: From and Into Traits
//!
//! `From<T>` and `TryFrom<T>` are the standard library's vocabulary for
//! "build a `Self` out of a `T`". They are the Rust counterpart to
//! OCaml's hand-rolled `foo_of_bar` helpers, with two important
//! upgrades:
//!
//! * implementing `From<T> for U` automatically gives you `Into<U> for T`
//! for free, so callers can write `let f: Fahrenheit = c.into();`
//! without you having to define `into` yourself, and
//! * `TryFrom<T>` carves out the fallible case behind a typed error,
//! replacing OCaml's ad-hoc `Result` returns with a single uniform
//! trait that any generic code can require.
//!
//! This module shows three idioms:
//!
//! * a pair of newtypes with infallible `From` impls in both directions,
//! * an enum with a `TryFrom<&str>` impl plus a back-conversion to
//! `&'static str`,
//! * a record-to-record conversion that may fail, expressed via
//! `TryFrom` and a dedicated error enum.
//!
//! The error type for `RawUser` -> `User` is a real enum (rather than a
//! `String`) to demonstrate the idiomatic shape of a fallible
//! conversion.
use std::fmt;
use std::num::ParseIntError;
// ---------------------------------------------------------------------------
// Approach 1: Newtypes with infallible `From` in both directions
// ---------------------------------------------------------------------------
/// A temperature in degrees Celsius.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Celsius(pub f64);
/// A temperature in degrees Fahrenheit.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Fahrenheit(pub f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
// ---------------------------------------------------------------------------
// Approach 2: `TryFrom<&str>` for parsing, `From<Color>` for rendering
// ---------------------------------------------------------------------------
/// A primary color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Red,
Green,
Blue,
}
/// The error returned when a string cannot be parsed as a [`Color`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownColor(pub String);
impl fmt::Display for UnknownColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unknown color: {}", self.0)
}
}
impl std::error::Error for UnknownColor {}
impl TryFrom<&str> for Color {
type Error = UnknownColor;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"blue" => Ok(Color::Blue),
other => Err(UnknownColor(other.to_string())),
}
}
}
impl From<Color> for &'static str {
fn from(c: Color) -> Self {
match c {
Color::Red => "red",
Color::Green => "green",
Color::Blue => "blue",
}
}
}
// ---------------------------------------------------------------------------
// Approach 3: Fallible record-to-record conversion
// ---------------------------------------------------------------------------
/// A user as it arrives from the outside world: every field is a string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawUser {
pub name: String,
pub age: String,
pub email: String,
}
/// A user after validation, with `age` parsed into a real integer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub name: String,
pub age: u32,
pub email: String,
}
/// Why a [`RawUser`] could not be converted into a [`User`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError {
/// The `age` field was not a valid non-negative integer.
InvalidAge(String),
}
impl fmt::Display for UserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserError::InvalidAge(s) => write!(f, "Invalid age: {s}"),
}
}
}
impl std::error::Error for UserError {}
impl From<ParseIntError> for UserError {
fn from(_: ParseIntError) -> Self {
UserError::InvalidAge(String::new())
}
}
impl TryFrom<RawUser> for User {
type Error = UserError;
fn try_from(raw: RawUser) -> Result<Self, Self::Error> {
let age = raw
.age
.parse::<u32>()
.map_err(|_| UserError::InvalidAge(raw.age.clone()))?;
Ok(User {
name: raw.name,
age,
email: raw.email,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// --- Approach 1: Temperature conversions --------------------------------
#[test]
fn celsius_to_fahrenheit_via_from() {
let f = Fahrenheit::from(Celsius(100.0));
assert!((f.0 - 212.0).abs() < 1e-9);
}
#[test]
fn celsius_to_fahrenheit_via_into() {
let f: Fahrenheit = Celsius(0.0).into();
assert!((f.0 - 32.0).abs() < 1e-9);
}
#[test]
fn fahrenheit_to_celsius_via_from() {
let c = Celsius::from(Fahrenheit(32.0));
assert!(c.0.abs() < 1e-9);
}
#[test]
fn temperature_round_trip_is_identity() {
let original = Celsius(37.5);
let round_tripped = Celsius::from(Fahrenheit::from(original));
assert!((round_tripped.0 - original.0).abs() < 1e-9);
}
// --- Approach 2: Color <-> string ---------------------------------------
#[test]
fn color_try_from_known_strings() {
assert_eq!(Color::try_from("red"), Ok(Color::Red));
assert_eq!(Color::try_from("green"), Ok(Color::Green));
assert_eq!(Color::try_from("blue"), Ok(Color::Blue));
}
#[test]
fn color_try_from_unknown_string_errors() {
assert_eq!(
Color::try_from("purple"),
Err(UnknownColor("purple".to_string()))
);
}
#[test]
fn unknown_color_display_includes_input() {
let err = Color::try_from("mauve").unwrap_err();
assert_eq!(err.to_string(), "Unknown color: mauve");
}
#[test]
fn color_to_static_str() {
let s: &'static str = Color::Blue.into();
assert_eq!(s, "blue");
}
#[test]
fn color_string_round_trip() {
for c in [Color::Red, Color::Green, Color::Blue] {
let s: &'static str = c.into();
assert_eq!(Color::try_from(s), Ok(c));
}
}
// --- Approach 3: RawUser -> User ----------------------------------------
#[test]
fn user_try_from_valid_raw() {
let raw = RawUser {
name: "Alice".to_string(),
age: "30".to_string(),
email: "a@b.com".to_string(),
};
let user = User::try_from(raw).expect("valid raw user");
assert_eq!(
user,
User {
name: "Alice".to_string(),
age: 30,
email: "a@b.com".to_string(),
}
);
}
#[test]
fn user_try_from_invalid_age_errors() {
let raw = RawUser {
name: "Bob".to_string(),
age: "xyz".to_string(),
email: "b@c.com".to_string(),
};
assert_eq!(
User::try_from(raw),
Err(UserError::InvalidAge("xyz".to_string()))
);
}
#[test]
fn user_error_display_message() {
let raw = RawUser {
name: "Bob".to_string(),
age: "xyz".to_string(),
email: "b@c.com".to_string(),
};
let err = User::try_from(raw).unwrap_err();
assert_eq!(err.to_string(), "Invalid age: xyz");
}
#[test]
fn user_try_from_negative_age_errors() {
// `u32::from_str` rejects negative inputs, so "-1" is invalid.
let raw = RawUser {
name: "Eve".to_string(),
age: "-1".to_string(),
email: "e@x.com".to_string(),
};
assert!(User::try_from(raw).is_err());
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Infallible | impl From<A> for B | b_of_a : a -> b function |
| Fallible | impl TryFrom<A> for B | b_of_a : a -> ('b, err) result |
| Ergonomics | .into() call | Explicit function call |
| Generic over conversion | Trait bound From<A> | Higher-order function parameter |
| Auto-blanket | Into from From | Manual |
| Code reuse | One From impl for all call sites | One function, explicit at each call |
The From/Into system is one of Rust's most pervasive patterns. Standard library types extensively use it: String::from("hello"), Vec::from([1, 2, 3]), error propagation with ?. Implementing From for your types integrates them into this ecosystem.
OCaml Approach
OCaml uses plain functions: fahrenheit_of_celsius, celsius_of_fahrenheit, color_of_string, user_of_raw. There is no trait system to unify these under a single interface. Code that needs to be generic over conversions must take the conversion function as a parameter. The result type mirrors Rust: Ok and Error are standard OCaml result constructors, making Result.bind chains natural.
Full Source
//! 084: From and Into Traits
//!
//! `From<T>` and `TryFrom<T>` are the standard library's vocabulary for
//! "build a `Self` out of a `T`". They are the Rust counterpart to
//! OCaml's hand-rolled `foo_of_bar` helpers, with two important
//! upgrades:
//!
//! * implementing `From<T> for U` automatically gives you `Into<U> for T`
//! for free, so callers can write `let f: Fahrenheit = c.into();`
//! without you having to define `into` yourself, and
//! * `TryFrom<T>` carves out the fallible case behind a typed error,
//! replacing OCaml's ad-hoc `Result` returns with a single uniform
//! trait that any generic code can require.
//!
//! This module shows three idioms:
//!
//! * a pair of newtypes with infallible `From` impls in both directions,
//! * an enum with a `TryFrom<&str>` impl plus a back-conversion to
//! `&'static str`,
//! * a record-to-record conversion that may fail, expressed via
//! `TryFrom` and a dedicated error enum.
//!
//! The error type for `RawUser` -> `User` is a real enum (rather than a
//! `String`) to demonstrate the idiomatic shape of a fallible
//! conversion.
use std::fmt;
use std::num::ParseIntError;
// ---------------------------------------------------------------------------
// Approach 1: Newtypes with infallible `From` in both directions
// ---------------------------------------------------------------------------
/// A temperature in degrees Celsius.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Celsius(pub f64);
/// A temperature in degrees Fahrenheit.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Fahrenheit(pub f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
// ---------------------------------------------------------------------------
// Approach 2: `TryFrom<&str>` for parsing, `From<Color>` for rendering
// ---------------------------------------------------------------------------
/// A primary color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Red,
Green,
Blue,
}
/// The error returned when a string cannot be parsed as a [`Color`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownColor(pub String);
impl fmt::Display for UnknownColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unknown color: {}", self.0)
}
}
impl std::error::Error for UnknownColor {}
impl TryFrom<&str> for Color {
type Error = UnknownColor;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"blue" => Ok(Color::Blue),
other => Err(UnknownColor(other.to_string())),
}
}
}
impl From<Color> for &'static str {
fn from(c: Color) -> Self {
match c {
Color::Red => "red",
Color::Green => "green",
Color::Blue => "blue",
}
}
}
// ---------------------------------------------------------------------------
// Approach 3: Fallible record-to-record conversion
// ---------------------------------------------------------------------------
/// A user as it arrives from the outside world: every field is a string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawUser {
pub name: String,
pub age: String,
pub email: String,
}
/// A user after validation, with `age` parsed into a real integer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub name: String,
pub age: u32,
pub email: String,
}
/// Why a [`RawUser`] could not be converted into a [`User`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError {
/// The `age` field was not a valid non-negative integer.
InvalidAge(String),
}
impl fmt::Display for UserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserError::InvalidAge(s) => write!(f, "Invalid age: {s}"),
}
}
}
impl std::error::Error for UserError {}
impl From<ParseIntError> for UserError {
fn from(_: ParseIntError) -> Self {
UserError::InvalidAge(String::new())
}
}
impl TryFrom<RawUser> for User {
type Error = UserError;
fn try_from(raw: RawUser) -> Result<Self, Self::Error> {
let age = raw
.age
.parse::<u32>()
.map_err(|_| UserError::InvalidAge(raw.age.clone()))?;
Ok(User {
name: raw.name,
age,
email: raw.email,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// --- Approach 1: Temperature conversions --------------------------------
#[test]
fn celsius_to_fahrenheit_via_from() {
let f = Fahrenheit::from(Celsius(100.0));
assert!((f.0 - 212.0).abs() < 1e-9);
}
#[test]
fn celsius_to_fahrenheit_via_into() {
let f: Fahrenheit = Celsius(0.0).into();
assert!((f.0 - 32.0).abs() < 1e-9);
}
#[test]
fn fahrenheit_to_celsius_via_from() {
let c = Celsius::from(Fahrenheit(32.0));
assert!(c.0.abs() < 1e-9);
}
#[test]
fn temperature_round_trip_is_identity() {
let original = Celsius(37.5);
let round_tripped = Celsius::from(Fahrenheit::from(original));
assert!((round_tripped.0 - original.0).abs() < 1e-9);
}
// --- Approach 2: Color <-> string ---------------------------------------
#[test]
fn color_try_from_known_strings() {
assert_eq!(Color::try_from("red"), Ok(Color::Red));
assert_eq!(Color::try_from("green"), Ok(Color::Green));
assert_eq!(Color::try_from("blue"), Ok(Color::Blue));
}
#[test]
fn color_try_from_unknown_string_errors() {
assert_eq!(
Color::try_from("purple"),
Err(UnknownColor("purple".to_string()))
);
}
#[test]
fn unknown_color_display_includes_input() {
let err = Color::try_from("mauve").unwrap_err();
assert_eq!(err.to_string(), "Unknown color: mauve");
}
#[test]
fn color_to_static_str() {
let s: &'static str = Color::Blue.into();
assert_eq!(s, "blue");
}
#[test]
fn color_string_round_trip() {
for c in [Color::Red, Color::Green, Color::Blue] {
let s: &'static str = c.into();
assert_eq!(Color::try_from(s), Ok(c));
}
}
// --- Approach 3: RawUser -> User ----------------------------------------
#[test]
fn user_try_from_valid_raw() {
let raw = RawUser {
name: "Alice".to_string(),
age: "30".to_string(),
email: "a@b.com".to_string(),
};
let user = User::try_from(raw).expect("valid raw user");
assert_eq!(
user,
User {
name: "Alice".to_string(),
age: 30,
email: "a@b.com".to_string(),
}
);
}
#[test]
fn user_try_from_invalid_age_errors() {
let raw = RawUser {
name: "Bob".to_string(),
age: "xyz".to_string(),
email: "b@c.com".to_string(),
};
assert_eq!(
User::try_from(raw),
Err(UserError::InvalidAge("xyz".to_string()))
);
}
#[test]
fn user_error_display_message() {
let raw = RawUser {
name: "Bob".to_string(),
age: "xyz".to_string(),
email: "b@c.com".to_string(),
};
let err = User::try_from(raw).unwrap_err();
assert_eq!(err.to_string(), "Invalid age: xyz");
}
#[test]
fn user_try_from_negative_age_errors() {
// `u32::from_str` rejects negative inputs, so "-1" is invalid.
let raw = RawUser {
name: "Eve".to_string(),
age: "-1".to_string(),
email: "e@x.com".to_string(),
};
assert!(User::try_from(raw).is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
// --- Approach 1: Temperature conversions --------------------------------
#[test]
fn celsius_to_fahrenheit_via_from() {
let f = Fahrenheit::from(Celsius(100.0));
assert!((f.0 - 212.0).abs() < 1e-9);
}
#[test]
fn celsius_to_fahrenheit_via_into() {
let f: Fahrenheit = Celsius(0.0).into();
assert!((f.0 - 32.0).abs() < 1e-9);
}
#[test]
fn fahrenheit_to_celsius_via_from() {
let c = Celsius::from(Fahrenheit(32.0));
assert!(c.0.abs() < 1e-9);
}
#[test]
fn temperature_round_trip_is_identity() {
let original = Celsius(37.5);
let round_tripped = Celsius::from(Fahrenheit::from(original));
assert!((round_tripped.0 - original.0).abs() < 1e-9);
}
// --- Approach 2: Color <-> string ---------------------------------------
#[test]
fn color_try_from_known_strings() {
assert_eq!(Color::try_from("red"), Ok(Color::Red));
assert_eq!(Color::try_from("green"), Ok(Color::Green));
assert_eq!(Color::try_from("blue"), Ok(Color::Blue));
}
#[test]
fn color_try_from_unknown_string_errors() {
assert_eq!(
Color::try_from("purple"),
Err(UnknownColor("purple".to_string()))
);
}
#[test]
fn unknown_color_display_includes_input() {
let err = Color::try_from("mauve").unwrap_err();
assert_eq!(err.to_string(), "Unknown color: mauve");
}
#[test]
fn color_to_static_str() {
let s: &'static str = Color::Blue.into();
assert_eq!(s, "blue");
}
#[test]
fn color_string_round_trip() {
for c in [Color::Red, Color::Green, Color::Blue] {
let s: &'static str = c.into();
assert_eq!(Color::try_from(s), Ok(c));
}
}
// --- Approach 3: RawUser -> User ----------------------------------------
#[test]
fn user_try_from_valid_raw() {
let raw = RawUser {
name: "Alice".to_string(),
age: "30".to_string(),
email: "a@b.com".to_string(),
};
let user = User::try_from(raw).expect("valid raw user");
assert_eq!(
user,
User {
name: "Alice".to_string(),
age: 30,
email: "a@b.com".to_string(),
}
);
}
#[test]
fn user_try_from_invalid_age_errors() {
let raw = RawUser {
name: "Bob".to_string(),
age: "xyz".to_string(),
email: "b@c.com".to_string(),
};
assert_eq!(
User::try_from(raw),
Err(UserError::InvalidAge("xyz".to_string()))
);
}
#[test]
fn user_error_display_message() {
let raw = RawUser {
name: "Bob".to_string(),
age: "xyz".to_string(),
email: "b@c.com".to_string(),
};
let err = User::try_from(raw).unwrap_err();
assert_eq!(err.to_string(), "Invalid age: xyz");
}
#[test]
fn user_try_from_negative_age_errors() {
// `u32::from_str` rejects negative inputs, so "-1" is invalid.
let raw = RawUser {
name: "Eve".to_string(),
age: "-1".to_string(),
email: "e@x.com".to_string(),
};
assert!(User::try_from(raw).is_err());
}
}
Deep Comparison
Core Insight
From<T> defines how to create a type from T. Into<T> is the reverse view. Implementing From auto-provides Into. This replaces ad-hoc conversion functions with a unified protocol.
OCaml Approach
int_of_float, string_of_intof_* / to_* conventionsRust Approach
impl From<Source> for TargetInto comes free via blanket impl.into() for ergonomic conversionTryFrom/TryInto for fallible conversionsComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Conversion | of_string, to_int functions | From/Into traits |
| Infallible | Manual function | From/Into |
| Fallible | Return option/result | TryFrom/TryInto |
| Auto | No | Into from From |
Exercises
impl From<i32> for Color that maps 0 → Red, 1 → Green, 2 → Blue and panics otherwise. Then add TryFrom<i32> that returns Err instead of panicking.From<Vec<(String, i32)>> for a HashMap<String, i32>.Validated<T> newtype that wraps T and implement TryFrom<String> for Validated<Email> where Email is another newtype.RawConfig → ParsedConfig → ValidatedConfig, each step using TryFrom.convert functor Convert(S : sig type t end)(D : sig type t val of_s : S.t -> D.t end) and show how it compares to Rust's impl From<S> for D.