ExamplesBy LevelBy TopicLearning Paths
084 Intermediate

084 — From and Into Traits

Functional Programming

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

  • • Implement From<A> for B and gain Into<B> for A automatically
  • • Use TryFrom<&str> when conversion can fail, returning Result<Self, Self::Error>
  • • Chain map_err and ? in TryFrom implementations
  • • Understand the blanket impl: impl<T, U: From<T>> Into<T> for U
  • • Use .into() at call sites for ergonomic type conversion
  • • Map Rust's trait-based conversion system to OCaml's explicit function naming
  • Code 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

    AspectRustOCaml
    Infallibleimpl From<A> for Bb_of_a : a -> b function
    Fallibleimpl TryFrom<A> for Bb_of_a : a -> ('b, err) result
    Ergonomics.into() callExplicit function call
    Generic over conversionTrait bound From<A>Higher-order function parameter
    Auto-blanketInto from FromManual
    Code reuseOne From impl for all call sitesOne 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • • Manual conversion functions: int_of_float, string_of_int
  • • No unified conversion trait
  • • Module-level of_* / to_* conventions
  • Rust Approach

  • impl From<Source> for Target
  • Into comes free via blanket impl
  • .into() for ergonomic conversion
  • TryFrom/TryInto for fallible conversions
  • Comparison Table

    FeatureOCamlRust
    Conversionof_string, to_int functionsFrom/Into traits
    InfallibleManual functionFrom/Into
    FallibleReturn option/resultTryFrom/TryInto
    AutoNoInto from From

    Exercises

  • Add 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.
  • Implement From<Vec<(String, i32)>> for a HashMap<String, i32>.
  • Write a Validated<T> newtype that wraps T and implement TryFrom<String> for Validated<Email> where Email is another newtype.
  • Create a conversion chain: RawConfigParsedConfigValidatedConfig, each step using TryFrom.
  • In OCaml, write a 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.
  • Open Source Repos