082 — Type Aliases
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
type Name = ... in Rusttype AppResult<T> = Result<T, AppError>type Predicate<T> = Box<dyn Fn(&T) -> bool> for complex closure typestype 'a result_t = ('a, error) resultCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Syntax | type Name = Type | type name = type |
| Generic | type Foo<T> = ... | type 'a foo = ... |
| Transparency | Fully transparent | Fully transparent |
| vs newtype | struct Meters(f64) is opaque | type meters = Meters of float is opaque |
| Common use | io::Result<T>, Predicate<T> | 'a option, custom result aliases |
| Closure alias | Box<dyn Fn(...)> needed | First-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;
}
}#[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) resulttype t = int for simple aliasesRust Approach
type Result<T> = std::result::Result<T, MyError>;io::Result<T>)Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Syntax | type alias = original | type Alias = Original; |
| Transparent | Yes | Yes |
| Generic | type 'a t = ... | type T<A> = ... |
| New type? | No | No |
Exercises
type Matrix = Vec<Vec<f64>> and write a transpose(m: &Matrix) -> Matrix function using it.type Parser<T> = Box<dyn Fn(&str) -> Option<(T, &str)>> and implement a digit parser and a letter parser.type ResultVec<T, E> = Vec<Result<T, E>> and a function partition_results that splits it into (Vec<T>, Vec<E>).AppResult<i32> and call it with Result<i32, AppError> directly.type ('a, 'b) either = Left of 'a | Right of 'b and write a partition_eithers function. Compare this design with Rust's Result.