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
#![allow(clippy::all)]
// 082: Type Aliases
// Shortening complex types with type aliases
// Approach 1: Simple aliases
type Point = (f64, f64);
type Name = String;
fn distance(p1: Point, p2: Point) -> f64 {
((p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2)).sqrt()
}
// Approach 2: Result alias (common pattern)
#[derive(Debug, PartialEq)]
enum AppError {
ParseError(String),
DivByZero,
}
type AppResult<T> = Result<T, AppError>;
fn parse_int(s: &str) -> AppResult<i32> {
s.parse()
.map_err(|_| AppError::ParseError(format!("Not a number: {}", s)))
}
fn safe_div(a: i32, b: i32) -> AppResult<i32> {
if b == 0 {
Err(AppError::DivByZero)
} else {
Ok(a / b)
}
}
// Approach 3: Complex type aliases
type Predicate<T> = Box<dyn Fn(&T) -> bool>;
type Transform<T> = Box<dyn Fn(T) -> T>;
fn filter_map_custom<T: Clone, U>(
items: &[T],
pred: &dyn Fn(&T) -> bool,
f: &dyn Fn(T) -> U,
) -> Vec<U> {
items
.iter()
.filter(|x| pred(x))
.map(|x| f(x.clone()))
.collect()
}
// io::Result pattern
type IoResult<T> = std::io::Result<T>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance() {
assert!((distance((0.0, 0.0), (3.0, 4.0)) - 5.0).abs() < 0.001);
}
#[test]
fn test_parse_int() {
assert_eq!(parse_int("42"), Ok(42));
assert!(parse_int("abc").is_err());
}
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 3), Ok(3));
assert_eq!(safe_div(10, 0), Err(AppError::DivByZero));
}
#[test]
fn test_filter_map() {
let result = filter_map_custom(&[1, 2, 3, 4, 5, 6], &|x: &i32| x % 2 == 0, &|x: i32| x * 2);
assert_eq!(result, vec![4, 8, 12]);
}
}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
#![allow(clippy::all)]
// 082: Type Aliases
// Shortening complex types with type aliases
// Approach 1: Simple aliases
type Point = (f64, f64);
type Name = String;
fn distance(p1: Point, p2: Point) -> f64 {
((p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2)).sqrt()
}
// Approach 2: Result alias (common pattern)
#[derive(Debug, PartialEq)]
enum AppError {
ParseError(String),
DivByZero,
}
type AppResult<T> = Result<T, AppError>;
fn parse_int(s: &str) -> AppResult<i32> {
s.parse()
.map_err(|_| AppError::ParseError(format!("Not a number: {}", s)))
}
fn safe_div(a: i32, b: i32) -> AppResult<i32> {
if b == 0 {
Err(AppError::DivByZero)
} else {
Ok(a / b)
}
}
// Approach 3: Complex type aliases
type Predicate<T> = Box<dyn Fn(&T) -> bool>;
type Transform<T> = Box<dyn Fn(T) -> T>;
fn filter_map_custom<T: Clone, U>(
items: &[T],
pred: &dyn Fn(&T) -> bool,
f: &dyn Fn(T) -> U,
) -> Vec<U> {
items
.iter()
.filter(|x| pred(x))
.map(|x| f(x.clone()))
.collect()
}
// io::Result pattern
type IoResult<T> = std::io::Result<T>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance() {
assert!((distance((0.0, 0.0), (3.0, 4.0)) - 5.0).abs() < 0.001);
}
#[test]
fn test_parse_int() {
assert_eq!(parse_int("42"), Ok(42));
assert!(parse_int("abc").is_err());
}
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 3), Ok(3));
assert_eq!(safe_div(10, 0), Err(AppError::DivByZero));
}
#[test]
fn test_filter_map() {
let result = filter_map_custom(&[1, 2, 3, 4, 5, 6], &|x: &i32| x % 2 == 0, &|x: i32| x * 2);
assert_eq!(result, vec![4, 8, 12]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance() {
assert!((distance((0.0, 0.0), (3.0, 4.0)) - 5.0).abs() < 0.001);
}
#[test]
fn test_parse_int() {
assert_eq!(parse_int("42"), Ok(42));
assert!(parse_int("abc").is_err());
}
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 3), Ok(3));
assert_eq!(safe_div(10, 0), Err(AppError::DivByZero));
}
#[test]
fn test_filter_map() {
let result = filter_map_custom(&[1, 2, 3, 4, 5, 6], &|x: &i32| x % 2 == 0, &|x: i32| x * 2);
assert_eq!(result, vec![4, 8, 12]);
}
}
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.