404: From, Into, TryFrom, TryInto
Tutorial Video
Text description (accessibility)
This video demonstrates the "404: From, Into, TryFrom, TryInto" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Type conversions are pervasive: temperatures between Celsius and Fahrenheit, integers between types, domain values from raw data. Key difference from OCaml: 1. **Blanket impl**: Rust's `Into` is automatically derived from `From`; OCaml requires implementing each conversion direction independently.
Tutorial
The Problem
Type conversions are pervasive: temperatures between Celsius and Fahrenheit, integers between types, domain values from raw data. Ad-hoc conversion functions (celsius_to_fahrenheit, as_kelvin) don't compose and require memorizing function names. The From/Into trait pair standardizes infallible conversions: implement From<A> for B and get Into<A> on B for free via blanket impl. TryFrom/TryInto handle fallible conversions returning Result. This unification means all conversions use .into(), .from(), or .try_into() consistently.
From/Into power the entire ? operator error conversion, Into<Vec<u8>> in network APIs, From<String> for PathBuf, and virtually every constructor pattern in std.
🎯 Learning Outcomes
From/Into blanket impl relationship: implementing From<A> for B gives Into<B> for A automaticallyTryFrom/TryInto for fallible conversions with type ErrorFrom chains.into() sometimes requires type annotation to resolve ambiguityFrom powers the ? operator's error type conversionCode Example
use std::convert::TryFrom;
struct PositiveInt(u32);
impl TryFrom<i32> for PositiveInt {
type Error = &'static str;
fn try_from(n: i32) -> Result<Self, Self::Error> {
if n > 0 {
Ok(PositiveInt(n as u32))
} else {
Err("must be positive")
}
}
}
fn main() {
match PositiveInt::try_from(42) {
Ok(p) => println!("Got: {}", p.0),
Err(e) => println!("Error: {}", e),
}
}Key Differences
Into is automatically derived from From; OCaml requires implementing each conversion direction independently.? operator uses From<E1> for E2 for automatic error coercion; OCaml uses Result.map_error explicitly.TryFrom/TryInto for fallible conversions with type Error; OCaml uses option or result as return types on any function..into() (let f: Fahrenheit = c.into()); OCaml's explicit function names make the target type clear.OCaml Approach
OCaml uses explicit conversion functions in modules: Celsius.of_fahrenheit, Temperature.to_kelvin. There is no equivalent of the From/Into blanket relationship — each conversion function is independent. OCaml's Result.bind and let* syntax handle fallible conversions. The ppx_conv_func library provides some standardization but OCaml has no universal conversion trait.
Full Source
#![allow(clippy::all)]
//! From, Into, TryFrom, TryInto Traits
//!
//! Type conversion traits for infallible and fallible conversions.
use std::fmt;
/// Temperature in Celsius.
#[derive(Debug, Clone, PartialEq)]
pub struct Celsius(pub f64);
/// Temperature in Fahrenheit.
#[derive(Debug, Clone, PartialEq)]
pub struct Fahrenheit(pub f64);
/// Kelvin temperature.
#[derive(Debug, Clone, PartialEq)]
pub struct Kelvin(pub f64);
// Infallible conversions using From
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)
}
}
impl From<Celsius> for Kelvin {
fn from(c: Celsius) -> Self {
Kelvin(c.0 + 273.15)
}
}
impl From<Kelvin> for Celsius {
fn from(k: Kelvin) -> Self {
Celsius(k.0 - 273.15)
}
}
/// A validated positive integer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PositiveInt(u32);
impl PositiveInt {
/// Returns the inner value.
pub fn value(&self) -> u32 {
self.0
}
}
/// Error for non-positive values.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NotPositiveError;
impl fmt::Display for NotPositiveError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "value must be positive (> 0)")
}
}
impl std::error::Error for NotPositiveError {}
// Fallible conversion using TryFrom
impl TryFrom<i32> for PositiveInt {
type Error = NotPositiveError;
fn try_from(n: i32) -> Result<Self, Self::Error> {
if n > 0 {
Ok(PositiveInt(n as u32))
} else {
Err(NotPositiveError)
}
}
}
impl TryFrom<i64> for PositiveInt {
type Error = NotPositiveError;
fn try_from(n: i64) -> Result<Self, Self::Error> {
if n > 0 && n <= u32::MAX as i64 {
Ok(PositiveInt(n as u32))
} else {
Err(NotPositiveError)
}
}
}
/// A valid network port (1024-65535 for non-privileged).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Port(u16);
impl Port {
/// Returns the port number.
pub fn number(&self) -> u16 {
self.0
}
}
/// Error for invalid port values.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortError {
ParseError(String),
TooLow(u16),
}
impl fmt::Display for PortError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PortError::ParseError(s) => write!(f, "invalid port: {}", s),
PortError::TooLow(n) => write!(f, "port {} is below 1024", n),
}
}
}
impl TryFrom<&str> for Port {
type Error = PortError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let n: u16 = s
.parse()
.map_err(|_| PortError::ParseError(s.to_string()))?;
if n >= 1024 {
Ok(Port(n))
} else {
Err(PortError::TooLow(n))
}
}
}
impl TryFrom<u32> for Port {
type Error = PortError;
fn try_from(n: u32) -> Result<Self, Self::Error> {
if (1024..=65535).contains(&n) {
Ok(Port(n as u16))
} else if n < 1024 {
Err(PortError::TooLow(n as u16))
} else {
Err(PortError::ParseError(format!("{} exceeds u16", n)))
}
}
}
/// Demonstrates the `as` keyword for primitive casts.
pub fn primitive_casts() -> (i32, u8, f64) {
let a: i64 = 1000;
let b: i32 = a as i32; // truncating cast
let c: i32 = 300;
let d: u8 = c as u8; // wrapping cast (300 % 256 = 44)
let e: i32 = 42;
let f: f64 = e as f64; // widening cast
(b, d, f)
}
/// Generic function accepting anything convertible to String.
pub fn greet<S: Into<String>>(name: S) {
let name = name.into();
println!("Hello, {}!", name);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_celsius_to_fahrenheit() {
let c = Celsius(0.0);
let f: Fahrenheit = c.into();
assert!((f.0 - 32.0).abs() < 0.001);
}
#[test]
fn test_fahrenheit_to_celsius() {
let f = Fahrenheit(212.0);
let c = Celsius::from(f);
assert!((c.0 - 100.0).abs() < 0.001);
}
#[test]
fn test_celsius_to_kelvin() {
let c = Celsius(0.0);
let k: Kelvin = c.into();
assert!((k.0 - 273.15).abs() < 0.001);
}
#[test]
fn test_kelvin_to_celsius() {
let k = Kelvin(0.0);
let c: Celsius = k.into();
assert!((c.0 - (-273.15)).abs() < 0.001);
}
#[test]
fn test_positive_int_success() {
let p: Result<PositiveInt, _> = 42i32.try_into();
assert!(p.is_ok());
assert_eq!(p.unwrap().value(), 42);
}
#[test]
fn test_positive_int_zero_fails() {
let p: Result<PositiveInt, _> = 0i32.try_into();
assert!(p.is_err());
}
#[test]
fn test_positive_int_negative_fails() {
let p: Result<PositiveInt, _> = (-5i32).try_into();
assert!(p.is_err());
}
#[test]
fn test_port_valid() {
let p = Port::try_from("8080");
assert!(p.is_ok());
assert_eq!(p.unwrap().number(), 8080);
}
#[test]
fn test_port_below_1024() {
let p = Port::try_from("80");
assert!(matches!(p, Err(PortError::TooLow(80))));
}
#[test]
fn test_port_invalid_string() {
let p = Port::try_from("abc");
assert!(matches!(p, Err(PortError::ParseError(_))));
}
#[test]
fn test_port_from_u32() {
let p = Port::try_from(3000u32);
assert!(p.is_ok());
assert_eq!(p.unwrap().number(), 3000);
}
#[test]
fn test_primitive_casts() {
let (b, d, f) = primitive_casts();
assert_eq!(b, 1000);
assert_eq!(d, 44); // 300 % 256
assert_eq!(f, 42.0);
}
#[test]
fn test_stdlib_try_from() {
// Standard library TryFrom for primitive narrowing
let ok: Result<u8, _> = u8::try_from(100i32);
assert_eq!(ok, Ok(100u8));
let err: Result<u8, _> = u8::try_from(1000i32);
assert!(err.is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_celsius_to_fahrenheit() {
let c = Celsius(0.0);
let f: Fahrenheit = c.into();
assert!((f.0 - 32.0).abs() < 0.001);
}
#[test]
fn test_fahrenheit_to_celsius() {
let f = Fahrenheit(212.0);
let c = Celsius::from(f);
assert!((c.0 - 100.0).abs() < 0.001);
}
#[test]
fn test_celsius_to_kelvin() {
let c = Celsius(0.0);
let k: Kelvin = c.into();
assert!((k.0 - 273.15).abs() < 0.001);
}
#[test]
fn test_kelvin_to_celsius() {
let k = Kelvin(0.0);
let c: Celsius = k.into();
assert!((c.0 - (-273.15)).abs() < 0.001);
}
#[test]
fn test_positive_int_success() {
let p: Result<PositiveInt, _> = 42i32.try_into();
assert!(p.is_ok());
assert_eq!(p.unwrap().value(), 42);
}
#[test]
fn test_positive_int_zero_fails() {
let p: Result<PositiveInt, _> = 0i32.try_into();
assert!(p.is_err());
}
#[test]
fn test_positive_int_negative_fails() {
let p: Result<PositiveInt, _> = (-5i32).try_into();
assert!(p.is_err());
}
#[test]
fn test_port_valid() {
let p = Port::try_from("8080");
assert!(p.is_ok());
assert_eq!(p.unwrap().number(), 8080);
}
#[test]
fn test_port_below_1024() {
let p = Port::try_from("80");
assert!(matches!(p, Err(PortError::TooLow(80))));
}
#[test]
fn test_port_invalid_string() {
let p = Port::try_from("abc");
assert!(matches!(p, Err(PortError::ParseError(_))));
}
#[test]
fn test_port_from_u32() {
let p = Port::try_from(3000u32);
assert!(p.is_ok());
assert_eq!(p.unwrap().number(), 3000);
}
#[test]
fn test_primitive_casts() {
let (b, d, f) = primitive_casts();
assert_eq!(b, 1000);
assert_eq!(d, 44); // 300 % 256
assert_eq!(f, 42.0);
}
#[test]
fn test_stdlib_try_from() {
// Standard library TryFrom for primitive narrowing
let ok: Result<u8, _> = u8::try_from(100i32);
assert_eq!(ok, Ok(100u8));
let err: Result<u8, _> = u8::try_from(1000i32);
assert!(err.is_err());
}
}
Deep Comparison
OCaml vs Rust: From/Into Conversion Traits
Side-by-Side Code
OCaml — Module-based conversion
module type TryFrom = sig
type source
type target
type error
val try_from : source -> (target, error) result
end
module StringToInt : TryFrom
with type source = string
and type target = int
and type error = string = struct
type source = string
type target = int
type error = string
let try_from s =
match int_of_string_opt s with
| Some n -> Ok n
| None -> Error ("Not a number: " ^ s)
end
let () =
match StringToInt.try_from "42" with
| Ok n -> Printf.printf "Got: %d\n" n
| Error e -> Printf.printf "Error: %s\n" e
Rust — Trait-based conversion
use std::convert::TryFrom;
struct PositiveInt(u32);
impl TryFrom<i32> for PositiveInt {
type Error = &'static str;
fn try_from(n: i32) -> Result<Self, Self::Error> {
if n > 0 {
Ok(PositiveInt(n as u32))
} else {
Err("must be positive")
}
}
}
fn main() {
match PositiveInt::try_from(42) {
Ok(p) => println!("Got: {}", p.0),
Err(e) => println!("Error: {}", e),
}
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Infallible conversion | Float.of_int, Int.to_string | From trait, .into() |
| Fallible conversion | *_opt functions, custom modules | TryFrom trait, .try_into() |
| Generic bounds | Module functors | T: Into<U> trait bounds |
| Automatic impl | None | Into auto-derived from From |
| Error type | Part of result | Associated type Error |
| Primitive casts | Type-specific functions | as keyword |
From vs Into
// Implementing From gives you Into for free
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Fahrenheit {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
// Now both work:
let f1 = Fahrenheit::from(Celsius(100.0)); // From
let f2: Fahrenheit = Celsius(100.0).into(); // Into (auto-derived)
The idiom: **implement From, use Into in bounds**.
// Generic function accepting anything convertible
fn process<T: Into<String>>(input: T) {
let s: String = input.into();
// ...
}
process("literal"); // &str -> String
process(String::from("x")); // String -> String (no-op)
TryFrom vs TryInto
For fallible conversions:
impl TryFrom<&str> for Port {
type Error = PortError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let n: u16 = s.parse()?;
if n >= 1024 { Ok(Port(n)) }
else { Err(PortError::TooLow) }
}
}
// Usage
let p: Result<Port, _> = "8080".try_into();
let p2 = Port::try_from("80"); // Err
The as Keyword
Rust's as is for primitive casts only:
let a: i64 = 1000;
let b: i32 = a as i32; // truncating (safe here)
let c: i32 = 300;
let d: u8 = c as u8; // wrapping: 300 % 256 = 44 ⚠️
let e: f64 = 42i32 as f64; // widening (safe)
Warning: as can silently truncate! Use TryFrom when overflow matters.
5 Takeaways
From, use Into in bounds.** impl From<A> for B gives you impl Into<B> for A automatically.
TryFrom/TryInto are for fallible conversions.** Return Result with an associated error type.
Same concept, different implementation patterns.
as is only for primitives and can lose data.** It's fast but unchecked — use TryFrom for safety.
Into bounds make APIs flexible.** Accept impl Into<String> to take &str, String, etc.
Exercises
From<RgbColor> for HslColor and From<HslColor> for RgbColor using the standard formulas. Write a round-trip test verifying that converting RGB → HSL → RGB returns the original within floating-point tolerance.ParseError, IoError, AppError. Implement From<ParseError> for AppError and From<IoError> for AppError. Write a function using ? that combines a parse and an IO operation into a single Result<T, AppError>.From<i32> for BigInt(Vec<i32>) and TryFrom<BigInt> for i32 (failing when the BigInt doesn't fit). Show that let big: BigInt = 42i32.into() works, and i32::try_from(big).unwrap_or(-1) handles overflow.