ExamplesBy LevelBy TopicLearning Paths
081 Intermediate

081 — Newtype Pattern

Functional Programming

Tutorial

The Problem

Use single-field tuple structs (struct Meters(f64)) to give distinct types to values that share the same underlying representation. Prevent units-of-measure confusion (Meters vs Seconds), enforce distinct ID types (UserId vs OrderId), and add type-safe conversions between Celsius and Fahrenheit — all with zero runtime overhead.

🎯 Learning Outcomes

  • • Define newtypes as single-field tuple structs in Rust
  • • Understand that newtypes are zero-cost: no runtime overhead vs the wrapped type
  • • Prevent accidental mixing of same-representation values at compile time
  • • Implement From<Celsius> for Fahrenheit for ergonomic .into() conversions
  • • Map Rust newtypes to OCaml single-constructor variants (type meters = Meters of float)
  • • Recognise when newtypes add safety versus when they add friction
  • Code Example

    #![allow(clippy::all)]
    // 081: Newtype Pattern
    // Wrapping primitives for type safety at zero cost
    
    // Approach 1: Simple newtypes
    #[derive(Debug, Clone, Copy)]
    struct Meters(f64);
    
    #[derive(Debug, Clone, Copy)]
    struct Seconds(f64);
    
    #[derive(Debug, Clone, Copy)]
    struct MetersPerSecond(f64);
    
    fn speed(distance: Meters, time: Seconds) -> Option<MetersPerSecond> {
        if time.0 == 0.0 {
            None
        } else {
            Some(MetersPerSecond(distance.0 / time.0))
        }
    }
    
    // Approach 2: Distinct ID types
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct UserId(u64);
    
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct OrderId(u64);
    
    // These are different types — can't mix them!
    fn find_user(id: UserId) -> String {
        format!("User #{}", id.0)
    }
    fn find_order(id: OrderId) -> String {
        format!("Order #{}", id.0)
    }
    
    // Approach 3: Newtype with 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)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_speed() {
            let s = speed(Meters(100.0), Seconds(10.0)).unwrap();
            assert!((s.0 - 10.0).abs() < 0.001);
            assert!(speed(Meters(100.0), Seconds(0.0)).is_none());
        }
    
        #[test]
        fn test_distinct_ids() {
            assert_eq!(find_user(UserId(42)), "User #42");
            assert_eq!(find_order(OrderId(7)), "Order #7");
            // UserId(1) != OrderId(1) — different types, won't even compile if compared
        }
    
        #[test]
        fn test_temperature() {
            let f: Fahrenheit = Celsius(100.0).into();
            assert!((f.0 - 212.0).abs() < 0.001);
            let c: Celsius = Fahrenheit(32.0).into();
            assert!(c.0.abs() < 0.001);
        }
    }

    Key Differences

    AspectRustOCaml
    Syntaxstruct Meters(f64)type meters = Meters of float
    Runtime costZero (transparent layout)Possible boxing for float in records
    Unwrapping.0 field accessPattern match (Meters m)
    Conversionimpl From<A> for BNamed function to_celsius
    Ergonomics.into() / From::fromExplicit call
    Compile-time safetyFull (different types)Full (different constructors)

    Both languages provide the same semantic guarantee: you cannot pass a Meters value where a Seconds is expected. Rust adds the From/Into trait infrastructure for ergonomic conversions; OCaml relies on explicit function naming.

    OCaml Approach

    OCaml achieves the same safety with single-constructor variants: type meters = Meters of float. Pattern matching in function arguments (let speed (Meters d) (Seconds t)) destructures automatically. OCaml variants are slightly heavier than Rust newtypes in that they may not be unboxed in all contexts, but the safety guarantee is equivalent. The to_fahrenheit and to_celsius functions are explicit conversions — OCaml has no From/Into trait system, so conversions require naming.

    Full Source

    #![allow(clippy::all)]
    // 081: Newtype Pattern
    // Wrapping primitives for type safety at zero cost
    
    // Approach 1: Simple newtypes
    #[derive(Debug, Clone, Copy)]
    struct Meters(f64);
    
    #[derive(Debug, Clone, Copy)]
    struct Seconds(f64);
    
    #[derive(Debug, Clone, Copy)]
    struct MetersPerSecond(f64);
    
    fn speed(distance: Meters, time: Seconds) -> Option<MetersPerSecond> {
        if time.0 == 0.0 {
            None
        } else {
            Some(MetersPerSecond(distance.0 / time.0))
        }
    }
    
    // Approach 2: Distinct ID types
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct UserId(u64);
    
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct OrderId(u64);
    
    // These are different types — can't mix them!
    fn find_user(id: UserId) -> String {
        format!("User #{}", id.0)
    }
    fn find_order(id: OrderId) -> String {
        format!("Order #{}", id.0)
    }
    
    // Approach 3: Newtype with 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)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_speed() {
            let s = speed(Meters(100.0), Seconds(10.0)).unwrap();
            assert!((s.0 - 10.0).abs() < 0.001);
            assert!(speed(Meters(100.0), Seconds(0.0)).is_none());
        }
    
        #[test]
        fn test_distinct_ids() {
            assert_eq!(find_user(UserId(42)), "User #42");
            assert_eq!(find_order(OrderId(7)), "Order #7");
            // UserId(1) != OrderId(1) — different types, won't even compile if compared
        }
    
        #[test]
        fn test_temperature() {
            let f: Fahrenheit = Celsius(100.0).into();
            assert!((f.0 - 212.0).abs() < 0.001);
            let c: Celsius = Fahrenheit(32.0).into();
            assert!(c.0.abs() < 0.001);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_speed() {
            let s = speed(Meters(100.0), Seconds(10.0)).unwrap();
            assert!((s.0 - 10.0).abs() < 0.001);
            assert!(speed(Meters(100.0), Seconds(0.0)).is_none());
        }
    
        #[test]
        fn test_distinct_ids() {
            assert_eq!(find_user(UserId(42)), "User #42");
            assert_eq!(find_order(OrderId(7)), "Order #7");
            // UserId(1) != OrderId(1) — different types, won't even compile if compared
        }
    
        #[test]
        fn test_temperature() {
            let f: Fahrenheit = Celsius(100.0).into();
            assert!((f.0 - 212.0).abs() < 0.001);
            let c: Celsius = Fahrenheit(32.0).into();
            assert!(c.0.abs() < 0.001);
        }
    }

    Deep Comparison

    Core Insight

    A newtype wraps a primitive to create a distinct type. Meters(5.0) and Feet(5.0) are different types — the compiler prevents accidental mixing. Zero runtime overhead in both languages.

    OCaml Approach

  • • Private types in module signatures
  • type meters = Meters of float (single-variant)
  • • Module abstraction hides constructor
  • Rust Approach

  • • Tuple struct: struct Meters(f64)
  • • Can implement traits on the newtype
  • • Deref for ergonomic access (use sparingly)
  • Comparison Table

    FeatureOCamlRust
    Syntaxtype t = T of innerstruct T(inner);
    AccessPattern match.0 field access
    OverheadZeroZero
    Trait implN/ACan impl traits

    Exercises

  • Add a Kilometers(f64) newtype and implement From<Kilometers> for Meters and vice versa.
  • Create a NonEmptyString(String) newtype with a constructor fn new(s: String) -> Option<NonEmptyString> that returns None for empty strings.
  • Add std::ops::Add<Meters> for Meters so that two distances can be summed while still being Meters.
  • Implement std::fmt::Display for Celsius to print values as "100°C" and Fahrenheit as "212°F".
  • In OCaml, define a validated_email newtype and a smart constructor val make : string -> validated_email option. Compare the pattern with Rust's equivalent approach.
  • Open Source Repos