ExamplesBy LevelBy TopicLearning Paths
878 Intermediate

878-from-into-traits — From/Into Traits

Functional Programming

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

  • • Implement From<T> and understand how Into<T> comes for free
  • • Use TryFrom<T> for fallible conversions that return Result
  • • Recognize how the ? operator leverages From for error type conversion
  • • Implement bidirectional conversions between temperature units
  • • Compare Rust's trait-based conversions with OCaml's explicit conversion functions
  • Code 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

  • Bidirectional for free: Implementing From<A> for B gives Into<A> on B automatically; OCaml requires both functions to be written explicitly.
  • Error handling integration: Rust's ? uses From to convert errors; OCaml uses Result.map_error or explicit rebinding.
  • Fallible conversions: TryFrom/TryInto formalize fallible conversions; OCaml uses option/result-returning functions by convention.
  • Coherence rules: Rust's orphan rules restrict where 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 });
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Implement From<(f64, f64)> for a Vector2D struct, and From<Vector2D> for (f64, f64) for round-trip conversion.
  • Create a Color enum with RGB and HSL variants, and implement From<RgbColor> for HslColor using the standard conversion formula.
  • Implement TryFrom<&str> for an IpAddr enum with V4([u8; 4]) and V6([u8; 16]) variants, parsing both formats.
  • Open Source Repos