Algebraic Data Types
An Algebraic Data Type (ADT) is a composite type formed by combining other types using two fundamental algebraic operations: products (structs and tuples, which hold fields $A \text{ AND } B$ simultaneously) and sums (enums, which hold variant $A \text{ OR } B$ exclusively).
Sum Types vs. Product Types and Cardinality
The "algebra" in Algebraic Data Types refers to the number of distinct values (the cardinality, $|T|$) a type can inhabit:
| Algebraic Form | Rust Construct | Cardinality Formula | Example (bool has $|bool| = 2$, u8 has $|u8| = 256$) |
|---|---|---|---|
| Zero (Additive Identity) | enum Never {} / ! |
$0$ | Uninhabited type (cannot be instantiated) |
| One (Multiplicative Identity) | () or struct Unit; |
$1$ | Exactly one value: () |
| Product Type ($A \times B$) | (A, B) or struct S { a: A, b: B } |
$|A| \times |B|$ | (bool, u8) has $2 \times 256 = 512$ possible values |
| Sum Type ($A + B$) | enum E { Left(A), Right(B) } |
$|A| + |B|$ | Result<u8, bool> has $256 + 2 = 258$ possible values |
Option<T> and Result<T, E> as Canonical Sum Types
In the standard library, Option<T> and Result<T, E> are generic sum types that encode optionality and fallibility directly in the type system instead of using ambient null pointers or untracked exceptions:
pub enum Option<T> {
None, // 1 value (1)
Some(T), // |T| values
} // Total cardinality: |Option<T>| = 1 + |T|
pub enum Result<T, E> {
Ok(T), // |T| values
Err(E), // |E| values
} // Total cardinality: |Result<T, E>| = |T| + |E|
Product Type: (T, E) Sum Type: Result<T, E>
+-------------------+ +------------------------+
| Must contain BOTH | | Tag = 0 ──> Ok(T) |
| t: T AND | | OR |
| e: E | | Tag = 1 ──> Err(E) |
+-------------------+ +------------------------+
Making Illegal States Unrepresentable with Sum Types
Replacing loosely coupled Option product fields (struct Connection { socket: Option<Socket>, error: Option<String> }, which permits invalid combinations like both None or both Some) with a sum type forces every state to be valid by construction, and the compiler enforces exhaustive pattern matching:
#[derive(Debug)]
enum ConnectionState {
Connecting { attempt: u8 },
Connected { session_id: u64 },
Failed { reason: String },
}
fn describe(state: &ConnectionState) -> String {
match state {
ConnectionState::Connecting { attempt } => format!("Attempt #{attempt}"),
ConnectionState::Connected { session_id } => format!("Active session {session_id}"),
ConnectionState::Failed { reason } => format!("Error: {reason}"),
}
}
fn main() {
let states = [
ConnectionState::Connecting { attempt: 1 },
ConnectionState::Connected { session_id: 9001 },
ConnectionState::Failed {
reason: String::from("TLS handshake timeout"),
},
];
for s in &states {
println!("{}", describe(s));
}
}
Attempt #1
Active session 9001
Error: TLS handshake timeout