ExamplesBy LevelBy TopicLearning Paths
082 Intermediate

082 — Type Aliases

Functional Programming

Tutorial

The Problem

Use type aliases to give shorter, more descriptive names to complex type expressions. Define Point, Name, AppResult<T>, Predicate<T>, and Transform<T> — reducing repetition in signatures and improving readability — and compare with OCaml's equivalent type declarations.

🎯 Learning Outcomes

  • • Write type aliases with type Name = ... in Rust
  • • Create generic aliases like type AppResult<T> = Result<T, AppError>
  • • Understand that aliases are transparent: the compiler sees through them
  • • Distinguish type aliases (transparent) from newtypes (opaque wrapper structs)
  • • Use type Predicate<T> = Box<dyn Fn(&T) -> bool> for complex closure types
  • • Map Rust aliases to OCaml type 'a result_t = ('a, error) result
  • Code Example

    //! 082: Type Aliases
    //!
    //! A type alias introduces a new name for an existing type without creating
    //! a new distinct type. Unlike the newtype pattern (see 081), aliases are
    //! purely cosmetic — they improve readability but do not participate in
    //! type checking as a separate identity. Use them to shorten verbose
    //! generic signatures, document intent, or standardize a domain `Result`.
    
    // ---------------------------------------------------------------------------
    // Approach 1: Simple aliases — give short names to compound types
    // ---------------------------------------------------------------------------
    
    /// A 2-D point expressed as `(x, y)` coordinates.
    pub type Point = (f64, f64);
    
    /// Euclidean distance between two points.
    pub fn distance(p1: Point, p2: Point) -> f64 {
        let (x1, y1) = p1;
        let (x2, y2) = p2;
        ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
    }
    
    // ---------------------------------------------------------------------------
    // Approach 2: Domain Result alias — the canonical use of type aliases
    // ---------------------------------------------------------------------------
    
    /// Errors that this module's fallible operations may produce.
    #[derive(Debug, PartialEq, Eq)]
    pub enum AppError {
        ParseError(String),
        DivByZero,
    }
    
    /// `Result` specialized to `AppError`, so call sites can write
    /// `AppResult<T>` instead of `Result<T, AppError>`.
    pub type AppResult<T> = Result<T, AppError>;
    
    /// Parses an `i32` or returns `AppError::ParseError`.
    pub fn parse_int(s: &str) -> AppResult<i32> {
        s.parse()
            .map_err(|_| AppError::ParseError(format!("Not a number: {s}")))
    }
    
    /// Integer division that rejects a zero divisor.
    pub fn safe_div(a: i32, b: i32) -> AppResult<i32> {
        if b == 0 {
            Err(AppError::DivByZero)
        } else {
            Ok(a / b)
        }
    }
    
    // ---------------------------------------------------------------------------
    // Approach 3: Generic aliases for higher-order function signatures
    // ---------------------------------------------------------------------------
    
    /// A boxed predicate over references to `T`.
    pub type Predicate<T> = Box<dyn Fn(&T) -> bool>;
    
    /// A boxed mapping from `T` to `U`.
    pub type Mapper<T, U> = Box<dyn Fn(T) -> U>;
    
    /// Filters `items` by `pred` and then maps survivors through `f`.
    pub fn filter_map<T: Clone, U>(items: &[T], pred: &Predicate<T>, f: &Mapper<T, U>) -> Vec<U> {
        items
            .iter()
            .filter(|x| pred(x))
            .map(|x| f(x.clone()))
            .collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        const EPS: f64 = 1e-9;
    
        #[test]
        fn distance_between_origin_and_3_4_is_5() {
            assert!((distance((0.0, 0.0), (3.0, 4.0)) - 5.0).abs() < EPS);
        }
    
        #[test]
        fn distance_is_symmetric() {
            let a: Point = (1.0, 2.0);
            let b: Point = (4.0, 6.0);
            assert!((distance(a, b) - distance(b, a)).abs() < EPS);
        }
    
        #[test]
        fn distance_to_self_is_zero() {
            let p: Point = (7.5, -2.25);
            assert_eq!(distance(p, p), 0.0);
        }
    
        #[test]
        fn parse_int_accepts_valid_integer() {
            assert_eq!(parse_int("42"), Ok(42));
            assert_eq!(parse_int("-7"), Ok(-7));
        }
    
        #[test]
        fn parse_int_rejects_non_numeric() {
            assert_eq!(
                parse_int("abc"),
                Err(AppError::ParseError("Not a number: abc".to_string()))
            );
        }
    
        #[test]
        fn safe_div_computes_quotient() {
            assert_eq!(safe_div(10, 3), Ok(3));
        }
    
        #[test]
        fn safe_div_rejects_zero_divisor() {
            assert_eq!(safe_div(10, 0), Err(AppError::DivByZero));
        }
    
        #[test]
        fn filter_map_keeps_and_transforms_evens() {
            let is_even: Predicate<i32> = Box::new(|x| x % 2 == 0);
            let double: Mapper<i32, i32> = Box::new(|x| x * 2);
            let out = filter_map(&[1, 2, 3, 4, 5, 6], &is_even, &double);
            assert_eq!(out, vec![4, 8, 12]);
        }
    
        #[test]
        fn filter_map_can_change_output_type() {
            let nonempty: Predicate<String> = Box::new(|s| !s.is_empty());
            let to_len: Mapper<String, usize> = Box::new(|s| s.len());
            let input = vec!["ab".to_string(), "".to_string(), "hello".to_string()];
            let out = filter_map(&input, &nonempty, &to_len);
            assert_eq!(out, vec![2, 5]);
        }
    
        #[test]
        fn app_result_alias_equals_full_form() {
            // A value typed as `AppResult<i32>` is exactly a `Result<i32, AppError>`.
            let r: AppResult<i32> = Ok(1);
            let _same: Result<i32, AppError> = r;
        }
    }

    Key Differences

    AspectRustOCaml
    Syntaxtype Name = Typetype name = type
    Generictype Foo<T> = ...type 'a foo = ...
    TransparencyFully transparentFully transparent
    vs newtypestruct Meters(f64) is opaquetype meters = Meters of float is opaque
    Common useio::Result<T>, Predicate<T>'a option, custom result aliases
    Closure aliasBox<dyn Fn(...)> neededFirst-class function type 'a -> 'b

    The key distinction: type aliases are purely cosmetic and transparent; newtypes (tuple structs in Rust, single-constructor variants in OCaml) create genuinely new types with type-checking separation. Use an alias when you want shorter notation; use a newtype when you want compile-time separation.

    OCaml Approach

    OCaml's type point = float * float and type 'a predicate = 'a -> bool serve the same purpose. Parameterised aliases use the 'a syntax: type 'a result_t = ('a, error) result. OCaml's type system treats aliases as identical to their expansion — no subtyping, no coercion needed. The notation is slightly different ('a result_t vs AppResult<T>) but the semantics are identical.

    Full Source

    //! 082: Type Aliases
    //!
    //! A type alias introduces a new name for an existing type without creating
    //! a new distinct type. Unlike the newtype pattern (see 081), aliases are
    //! purely cosmetic — they improve readability but do not participate in
    //! type checking as a separate identity. Use them to shorten verbose
    //! generic signatures, document intent, or standardize a domain `Result`.
    
    // ---------------------------------------------------------------------------
    // Approach 1: Simple aliases — give short names to compound types
    // ---------------------------------------------------------------------------
    
    /// A 2-D point expressed as `(x, y)` coordinates.
    pub type Point = (f64, f64);
    
    /// Euclidean distance between two points.
    pub fn distance(p1: Point, p2: Point) -> f64 {
        let (x1, y1) = p1;
        let (x2, y2) = p2;
        ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
    }
    
    // ---------------------------------------------------------------------------
    // Approach 2: Domain Result alias — the canonical use of type aliases
    // ---------------------------------------------------------------------------
    
    /// Errors that this module's fallible operations may produce.
    #[derive(Debug, PartialEq, Eq)]
    pub enum AppError {
        ParseError(String),
        DivByZero,
    }
    
    /// `Result` specialized to `AppError`, so call sites can write
    /// `AppResult<T>` instead of `Result<T, AppError>`.
    pub type AppResult<T> = Result<T, AppError>;
    
    /// Parses an `i32` or returns `AppError::ParseError`.
    pub fn parse_int(s: &str) -> AppResult<i32> {
        s.parse()
            .map_err(|_| AppError::ParseError(format!("Not a number: {s}")))
    }
    
    /// Integer division that rejects a zero divisor.
    pub fn safe_div(a: i32, b: i32) -> AppResult<i32> {
        if b == 0 {
            Err(AppError::DivByZero)
        } else {
            Ok(a / b)
        }
    }
    
    // ---------------------------------------------------------------------------
    // Approach 3: Generic aliases for higher-order function signatures
    // ---------------------------------------------------------------------------
    
    /// A boxed predicate over references to `T`.
    pub type Predicate<T> = Box<dyn Fn(&T) -> bool>;
    
    /// A boxed mapping from `T` to `U`.
    pub type Mapper<T, U> = Box<dyn Fn(T) -> U>;
    
    /// Filters `items` by `pred` and then maps survivors through `f`.
    pub fn filter_map<T: Clone, U>(items: &[T], pred: &Predicate<T>, f: &Mapper<T, U>) -> Vec<U> {
        items
            .iter()
            .filter(|x| pred(x))
            .map(|x| f(x.clone()))
            .collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        const EPS: f64 = 1e-9;
    
        #[test]
        fn distance_between_origin_and_3_4_is_5() {
            assert!((distance((0.0, 0.0), (3.0, 4.0)) - 5.0).abs() < EPS);
        }
    
        #[test]
        fn distance_is_symmetric() {
            let a: Point = (1.0, 2.0);
            let b: Point = (4.0, 6.0);
            assert!((distance(a, b) - distance(b, a)).abs() < EPS);
        }
    
        #[test]
        fn distance_to_self_is_zero() {
            let p: Point = (7.5, -2.25);
            assert_eq!(distance(p, p), 0.0);
        }
    
        #[test]
        fn parse_int_accepts_valid_integer() {
            assert_eq!(parse_int("42"), Ok(42));
            assert_eq!(parse_int("-7"), Ok(-7));
        }
    
        #[test]
        fn parse_int_rejects_non_numeric() {
            assert_eq!(
                parse_int("abc"),
                Err(AppError::ParseError("Not a number: abc".to_string()))
            );
        }
    
        #[test]
        fn safe_div_computes_quotient() {
            assert_eq!(safe_div(10, 3), Ok(3));
        }
    
        #[test]
        fn safe_div_rejects_zero_divisor() {
            assert_eq!(safe_div(10, 0), Err(AppError::DivByZero));
        }
    
        #[test]
        fn filter_map_keeps_and_transforms_evens() {
            let is_even: Predicate<i32> = Box::new(|x| x % 2 == 0);
            let double: Mapper<i32, i32> = Box::new(|x| x * 2);
            let out = filter_map(&[1, 2, 3, 4, 5, 6], &is_even, &double);
            assert_eq!(out, vec![4, 8, 12]);
        }
    
        #[test]
        fn filter_map_can_change_output_type() {
            let nonempty: Predicate<String> = Box::new(|s| !s.is_empty());
            let to_len: Mapper<String, usize> = Box::new(|s| s.len());
            let input = vec!["ab".to_string(), "".to_string(), "hello".to_string()];
            let out = filter_map(&input, &nonempty, &to_len);
            assert_eq!(out, vec![2, 5]);
        }
    
        #[test]
        fn app_result_alias_equals_full_form() {
            // A value typed as `AppResult<i32>` is exactly a `Result<i32, AppError>`.
            let r: AppResult<i32> = Ok(1);
            let _same: Result<i32, AppError> = r;
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        const EPS: f64 = 1e-9;
    
        #[test]
        fn distance_between_origin_and_3_4_is_5() {
            assert!((distance((0.0, 0.0), (3.0, 4.0)) - 5.0).abs() < EPS);
        }
    
        #[test]
        fn distance_is_symmetric() {
            let a: Point = (1.0, 2.0);
            let b: Point = (4.0, 6.0);
            assert!((distance(a, b) - distance(b, a)).abs() < EPS);
        }
    
        #[test]
        fn distance_to_self_is_zero() {
            let p: Point = (7.5, -2.25);
            assert_eq!(distance(p, p), 0.0);
        }
    
        #[test]
        fn parse_int_accepts_valid_integer() {
            assert_eq!(parse_int("42"), Ok(42));
            assert_eq!(parse_int("-7"), Ok(-7));
        }
    
        #[test]
        fn parse_int_rejects_non_numeric() {
            assert_eq!(
                parse_int("abc"),
                Err(AppError::ParseError("Not a number: abc".to_string()))
            );
        }
    
        #[test]
        fn safe_div_computes_quotient() {
            assert_eq!(safe_div(10, 3), Ok(3));
        }
    
        #[test]
        fn safe_div_rejects_zero_divisor() {
            assert_eq!(safe_div(10, 0), Err(AppError::DivByZero));
        }
    
        #[test]
        fn filter_map_keeps_and_transforms_evens() {
            let is_even: Predicate<i32> = Box::new(|x| x % 2 == 0);
            let double: Mapper<i32, i32> = Box::new(|x| x * 2);
            let out = filter_map(&[1, 2, 3, 4, 5, 6], &is_even, &double);
            assert_eq!(out, vec![4, 8, 12]);
        }
    
        #[test]
        fn filter_map_can_change_output_type() {
            let nonempty: Predicate<String> = Box::new(|s| !s.is_empty());
            let to_len: Mapper<String, usize> = Box::new(|s| s.len());
            let input = vec!["ab".to_string(), "".to_string(), "hello".to_string()];
            let out = filter_map(&input, &nonempty, &to_len);
            assert_eq!(out, vec![2, 5]);
        }
    
        #[test]
        fn app_result_alias_equals_full_form() {
            // A value typed as `AppResult<i32>` is exactly a `Result<i32, AppError>`.
            let r: AppResult<i32> = Ok(1);
            let _same: Result<i32, AppError> = r;
        }
    }

    Deep Comparison

    Core Insight

    Type aliases give shorter names to complex types. They're transparent — the compiler treats them as identical to the original. Useful for Result types, complex generics, and documentation.

    OCaml Approach

  • type 'a my_result = ('a, error) result
  • • Aliases are fully transparent
  • • Can also use type t = int for simple aliases
  • Rust Approach

  • type Result<T> = std::result::Result<T, MyError>;
  • • Common in library APIs (io::Result<T>)
  • • No new type created — just a name
  • Comparison Table

    FeatureOCamlRust
    Syntaxtype alias = originaltype Alias = Original;
    TransparentYesYes
    Generictype 'a t = ...type T<A> = ...
    New type?NoNo

    Exercises

  • Define type Matrix = Vec<Vec<f64>> and write a transpose(m: &Matrix) -> Matrix function using it.
  • Create type Parser<T> = Box<dyn Fn(&str) -> Option<(T, &str)>> and implement a digit parser and a letter parser.
  • Write type ResultVec<T, E> = Vec<Result<T, E>> and a function partition_results that splits it into (Vec<T>, Vec<E>).
  • Demonstrate the transparency: write a function that accepts AppResult<i32> and call it with Result<i32, AppError> directly.
  • In OCaml, define type ('a, 'b) either = Left of 'a | Right of 'b and write a partition_eithers function. Compare this design with Rust's Result.
  • Open Source Repos