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
#![allow(clippy::all)]
// 084: From and Into Traits
// Approach 1: Temperature conversion
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(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: Enum from string (TryFrom)
#[derive(Debug, PartialEq)]
enum Color {
Red,
Green,
Blue,
}
impl TryFrom<&str> for Color {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"blue" => Ok(Color::Blue),
_ => Err(format!("Unknown color: {}", s)),
}
}
}
impl From<Color> for &str {
fn from(c: Color) -> Self {
match c {
Color::Red => "red",
Color::Green => "green",
Color::Blue => "blue",
}
}
}
// Approach 3: From for complex types
struct RawUser {
name: String,
age: String,
email: String,
}
#[derive(Debug, PartialEq)]
struct User {
name: String,
age: u32,
email: String,
}
impl TryFrom<RawUser> for User {
type Error = String;
fn try_from(raw: RawUser) -> Result<Self, Self::Error> {
let age = raw.age.parse().map_err(|_| "Invalid age".to_string())?;
Ok(User {
name: raw.name,
age,
email: raw.email,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_celsius_to_fahrenheit() {
let f: Fahrenheit = Celsius(100.0).into();
assert!((f.0 - 212.0).abs() < 0.001);
}
#[test]
fn test_fahrenheit_to_celsius() {
let c: Celsius = Fahrenheit(32.0).into();
assert!(c.0.abs() < 0.001);
}
#[test]
fn test_color_try_from() {
assert_eq!(Color::try_from("red"), Ok(Color::Red));
assert!(Color::try_from("purple").is_err());
}
#[test]
fn test_user_try_from() {
let raw = RawUser {
name: "Alice".into(),
age: "30".into(),
email: "a@b.com".into(),
};
let user = User::try_from(raw).unwrap();
assert_eq!(user.age, 30);
}
#[test]
fn test_user_invalid() {
let raw = RawUser {
name: "Bob".into(),
age: "xyz".into(),
email: "b@c.com".into(),
};
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
#![allow(clippy::all)]
// 084: From and Into Traits
// Approach 1: Temperature conversion
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(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: Enum from string (TryFrom)
#[derive(Debug, PartialEq)]
enum Color {
Red,
Green,
Blue,
}
impl TryFrom<&str> for Color {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"blue" => Ok(Color::Blue),
_ => Err(format!("Unknown color: {}", s)),
}
}
}
impl From<Color> for &str {
fn from(c: Color) -> Self {
match c {
Color::Red => "red",
Color::Green => "green",
Color::Blue => "blue",
}
}
}
// Approach 3: From for complex types
struct RawUser {
name: String,
age: String,
email: String,
}
#[derive(Debug, PartialEq)]
struct User {
name: String,
age: u32,
email: String,
}
impl TryFrom<RawUser> for User {
type Error = String;
fn try_from(raw: RawUser) -> Result<Self, Self::Error> {
let age = raw.age.parse().map_err(|_| "Invalid age".to_string())?;
Ok(User {
name: raw.name,
age,
email: raw.email,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_celsius_to_fahrenheit() {
let f: Fahrenheit = Celsius(100.0).into();
assert!((f.0 - 212.0).abs() < 0.001);
}
#[test]
fn test_fahrenheit_to_celsius() {
let c: Celsius = Fahrenheit(32.0).into();
assert!(c.0.abs() < 0.001);
}
#[test]
fn test_color_try_from() {
assert_eq!(Color::try_from("red"), Ok(Color::Red));
assert!(Color::try_from("purple").is_err());
}
#[test]
fn test_user_try_from() {
let raw = RawUser {
name: "Alice".into(),
age: "30".into(),
email: "a@b.com".into(),
};
let user = User::try_from(raw).unwrap();
assert_eq!(user.age, 30);
}
#[test]
fn test_user_invalid() {
let raw = RawUser {
name: "Bob".into(),
age: "xyz".into(),
email: "b@c.com".into(),
};
assert!(User::try_from(raw).is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_celsius_to_fahrenheit() {
let f: Fahrenheit = Celsius(100.0).into();
assert!((f.0 - 212.0).abs() < 0.001);
}
#[test]
fn test_fahrenheit_to_celsius() {
let c: Celsius = Fahrenheit(32.0).into();
assert!(c.0.abs() < 0.001);
}
#[test]
fn test_color_try_from() {
assert_eq!(Color::try_from("red"), Ok(Color::Red));
assert!(Color::try_from("purple").is_err());
}
#[test]
fn test_user_try_from() {
let raw = RawUser {
name: "Alice".into(),
age: "30".into(),
email: "a@b.com".into(),
};
let user = User::try_from(raw).unwrap();
assert_eq!(user.age, 30);
}
#[test]
fn test_user_invalid() {
let raw = RawUser {
name: "Bob".into(),
age: "xyz".into(),
email: "b@c.com".into(),
};
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.