878-from-into-traits — From/Into Traits
Tutorial
The Problem
Type conversions are ubiquitous in systems programming: parsing strings, converting between unit systems, adapting error types. Rust's From<T> and Into<T> traits standardize these conversions. Implementing From<A> for B automatically provides Into<B> for A via a blanket implementation. The ? operator uses From to convert error types in fallible functions. TryFrom/TryInto handle conversions that can fail. This design replaces the error-prone cast operators of C/C++ with explicit, nameable, testable conversion functions. OCaml handles conversions through explicit functions in module interfaces, with no universal conversion trait.
🎯 Learning Outcomes
From<T> and understand how Into<T> comes for freeTryFrom<T> for fallible conversions that return Result? operator leverages From for error type conversionCode Example
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
// Into<Celsius> for Fahrenheit comes free!
let c: Celsius = Fahrenheit(212.0).into();Key Differences
From<A> for B gives Into<A> on B automatically; OCaml requires both functions to be written explicitly.? uses From to convert errors; OCaml uses Result.map_error or explicit rebinding.TryFrom/TryInto formalize fallible conversions; OCaml uses option/result-returning functions by convention.From can be implemented; OCaml has no such restriction on conversion functions.OCaml Approach
OCaml uses explicit named functions for conversions: celsius_to_fahrenheit: float -> float, fahrenheit_to_celsius: float -> float. There is no equivalent to Rust's blanket Into implementation. Error conversion in OCaml uses Result.map_error or explicit match on the inner error and rewrapping. The ppx_deriving.conv library can auto-derive conversion functions for record types. OCaml's type system does not enforce a canonical conversion interface.
Full Source
#![allow(clippy::all)]
// Example 084: From/Into Traits
// OCaml coercion → Rust explicit conversions
use std::fmt;
// === Approach 1: From trait for type conversions ===
#[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)
}
}
impl fmt::Display for Celsius {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.1}°C", self.0)
}
}
impl fmt::Display for Fahrenheit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.1}°F", self.0)
}
}
// === Approach 2: From for string parsing (TryFrom for fallible) ===
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl TryFrom<&str> for Point {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let s = s.trim_start_matches('(').trim_end_matches(')');
let parts: Vec<&str> = s.split(',').map(str::trim).collect();
if parts.len() != 2 {
return Err("Expected (x, y)".to_string());
}
let x = parts[0]
.parse()
.map_err(|e: std::num::ParseIntError| e.to_string())?;
let y = parts[1]
.parse()
.map_err(|e: std::num::ParseIntError| e.to_string())?;
Ok(Point { x, y })
}
}
// From<Point> for (i32, i32)
impl From<Point> for (i32, i32) {
fn from(p: Point) -> Self {
(p.x, p.y)
}
}
impl From<(i32, i32)> for Point {
fn from((x, y): (i32, i32)) -> Self {
Point { x, y }
}
}
// === Approach 3: Into in generic contexts ===
fn print_temperature<T: Into<Celsius>>(temp: T) {
let c: Celsius = temp.into();
println!("Temperature: {}", c);
}
// From/Into chain
fn fahrenheit_string_to_celsius(s: &str) -> Result<String, String> {
let val: f64 = s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string())?;
let c: Celsius = Fahrenheit(val).into(); // Into comes free from From
Ok(format!("{}", c))
}
// Collecting with From
fn strings_to_points(data: &[(i32, i32)]) -> Vec<Point> {
data.iter().copied().map(Point::from).collect()
}
#[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() < 1e-10);
}
#[test]
fn test_fahrenheit_to_celsius() {
let c: Celsius = Fahrenheit(32.0).into();
assert!((c.0 - 0.0).abs() < 1e-10);
}
#[test]
fn test_roundtrip() {
let original = Celsius(37.0);
let back: Celsius = Fahrenheit::from(original).into();
assert!((back.0 - 37.0).abs() < 1e-10);
}
#[test]
fn test_point_try_from() {
assert_eq!(Point::try_from("(3, 4)"), Ok(Point { x: 3, y: 4 }));
assert!(Point::try_from("invalid").is_err());
}
#[test]
fn test_point_from_tuple() {
let p: Point = (1, 2).into();
assert_eq!(p, Point { x: 1, y: 2 });
}
#[test]
fn test_tuple_from_point() {
let t: (i32, i32) = Point { x: 5, y: 6 }.into();
assert_eq!(t, (5, 6));
}
#[test]
fn test_fahrenheit_string_to_celsius() {
let result = fahrenheit_string_to_celsius("212");
assert_eq!(result, Ok("100.0°C".to_string()));
assert!(fahrenheit_string_to_celsius("abc").is_err());
}
#[test]
fn test_strings_to_points() {
let pts = strings_to_points(&[(1, 2), (3, 4)]);
assert_eq!(pts.len(), 2);
assert_eq!(pts[0], Point { x: 1, y: 2 });
}
}#[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() < 1e-10);
}
#[test]
fn test_fahrenheit_to_celsius() {
let c: Celsius = Fahrenheit(32.0).into();
assert!((c.0 - 0.0).abs() < 1e-10);
}
#[test]
fn test_roundtrip() {
let original = Celsius(37.0);
let back: Celsius = Fahrenheit::from(original).into();
assert!((back.0 - 37.0).abs() < 1e-10);
}
#[test]
fn test_point_try_from() {
assert_eq!(Point::try_from("(3, 4)"), Ok(Point { x: 3, y: 4 }));
assert!(Point::try_from("invalid").is_err());
}
#[test]
fn test_point_from_tuple() {
let p: Point = (1, 2).into();
assert_eq!(p, Point { x: 1, y: 2 });
}
#[test]
fn test_tuple_from_point() {
let t: (i32, i32) = Point { x: 5, y: 6 }.into();
assert_eq!(t, (5, 6));
}
#[test]
fn test_fahrenheit_string_to_celsius() {
let result = fahrenheit_string_to_celsius("212");
assert_eq!(result, Ok("100.0°C".to_string()));
assert!(fahrenheit_string_to_celsius("abc").is_err());
}
#[test]
fn test_strings_to_points() {
let pts = strings_to_points(&[(1, 2), (3, 4)]);
assert_eq!(pts.len(), 2);
assert_eq!(pts[0], Point { x: 1, y: 2 });
}
}
Deep Comparison
Comparison: From/Into Traits
Infallible Conversion
OCaml:
let fahrenheit_of_celsius c = { f = c.c *. 9.0 /. 5.0 +. 32.0 }
let celsius_of_fahrenheit f = { c = (f.f -. 32.0) *. 5.0 /. 9.0 }
Rust:
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
// Into<Celsius> for Fahrenheit comes free!
let c: Celsius = Fahrenheit(212.0).into();
Fallible Conversion
OCaml:
let int_of_string_opt s =
try Some (int_of_string s) with Failure _ -> None
Rust:
impl TryFrom<&str> for Point {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
// parse "(x, y)" format
}
}
let p = Point::try_from("(3, 4)")?;
Generic Into Bounds
OCaml:
(* Must pass conversion function explicitly *)
let print_celsius convert temp =
let c = convert temp in
Printf.printf "%.1f°C" c.c
Rust:
fn print_temperature<T: Into<Celsius>>(temp: T) {
let c: Celsius = temp.into();
println!("Temperature: {}", c);
}
print_temperature(Fahrenheit(98.6)); // auto-converts
print_temperature(Celsius(37.0)); // identity
Exercises
From<(f64, f64)> for a Vector2D struct, and From<Vector2D> for (f64, f64) for round-trip conversion.Color enum with RGB and HSL variants, and implement From<RgbColor> for HslColor using the standard conversion formula.TryFrom<&str> for an IpAddr enum with V4([u8; 4]) and V6([u8; 16]) variants, parsing both formats.