Length-Indexed Lists
Functional Programming
Tutorial
The Problem
Calling head on an empty list is a classic source of runtime errors. Length-indexed lists — vectors where the length is part of the type — eliminate this class of error: head is only defined on Vec2<T, N> where N > 0. Rust achieves this with const generics ([T; N]), which are more ergonomic than the type-level Peano natural approach (example 129) for most practical purposes. This example demonstrates the const-generic approach to length safety.
🎯 Learning Outcomes
const N: usize to encode list length in the type, making head on empty impossibleVec2<T, N> as a fixed-size wrapper with length-safe operationsappend type-safely combines two vectors: Vec2<T, N> + Vec2<T, M> → Vec2<T, {N+M}>N+M in const generics requires where [(); N+M]: boundsCode Example
struct Vec2<T, const N: usize> {
data: [T; N],
}Key Differences
head on n succ vec is guaranteed safe by the type; Rust's const-generic head uses a runtime assert! (a weaker guarantee).Add trait (example 129); Rust's {N+M} in const generics requires nightly generic_const_exprs.[T; N] is a built-in array with const generics; OCaml's length-indexed vector requires GADT definition.Vec<T> with runtime checks; const-generic arrays are used where performance and fixed sizes matter (embedded, SIMD).OCaml Approach
OCaml uses GADTs for length-indexed vectors:
type _ vec =
| Nil : zero vec
| Cons : 'a * 'n vec -> ('n succ) vec
let head : type n. (n succ) vec -> 'a = function Cons (x, _) -> x
head is only callable on non-empty vectors by construction — the succ in the type guarantees at least one element. There is no assert! and no runtime check; the guarantee is purely structural.
Full Source
#![allow(clippy::all)]
// Example 178: Length-Indexed Lists
// Rust uses const generics to track length at compile time
// === Approach 1: Const generic array wrapper ===
#[derive(Debug, Clone)]
struct Vec2<T, const N: usize> {
data: [T; N],
}
impl<T: Default + Copy, const N: usize> Vec2<T, N> {
fn replicate(val: T) -> Self {
Vec2 { data: [val; N] }
}
}
impl<T: Copy, const N: usize> Vec2<T, N> {
fn head(&self) -> T {
assert!(N > 0, "Cannot take head of empty Vec2");
self.data[0]
}
fn map<U: Default + Copy>(&self, f: impl Fn(T) -> U) -> Vec2<U, N> {
let mut result = [U::default(); N];
for i in 0..N {
result[i] = f(self.data[i]);
}
Vec2 { data: result }
}
}
fn zip_vec<T: Copy + Default, U: Copy + Default, const N: usize>(
a: &Vec2<T, N>,
b: &Vec2<U, N>,
) -> Vec2<(T, U), N> {
let mut result = [(T::default(), U::default()); N];
for i in 0..N {
result[i] = (a.data[i], b.data[i]);
}
Vec2 { data: result }
}
// === Approach 2: Type-level Peano numbers with nested tuples ===
trait Nat {}
struct Zero;
struct Succ<N: Nat>(std::marker::PhantomData<N>);
impl Nat for Zero {}
impl<N: Nat> Nat for Succ<N> {}
trait TypeVec<T> {
fn to_vec(&self) -> std::vec::Vec<T>
where
T: Clone;
}
#[derive(Debug)]
struct VNil;
#[derive(Debug)]
struct VCons<T, N: Nat, Rest: TypeVec<T>>(T, Rest, std::marker::PhantomData<N>);
impl<T: Clone> TypeVec<T> for VNil {
fn to_vec(&self) -> std::vec::Vec<T> {
vec![]
}
}
impl<T: Clone, N: Nat, Rest: TypeVec<T>> TypeVec<T> for VCons<T, N, Rest> {
fn to_vec(&self) -> std::vec::Vec<T> {
let mut v = vec![self.0.clone()];
v.extend(self.1.to_vec());
v
}
}
impl<T, N: Nat, Rest: TypeVec<T>> VCons<T, N, Rest> {
fn head(&self) -> &T {
&self.0
}
fn tail(&self) -> &Rest {
&self.1
}
}
fn vnil() -> VNil {
VNil
}
fn vcons<T, N: Nat, R: TypeVec<T>>(x: T, rest: R) -> VCons<T, Succ<N>, R>
where
R: TypeVec<T>,
{
VCons(x, rest, std::marker::PhantomData)
}
// === Approach 3: Recursive type with compile-time length encoding ===
trait SizedList {
type Elem;
const LEN: usize;
fn to_vec(&self) -> std::vec::Vec<Self::Elem>
where
Self::Elem: Clone;
}
struct LNil<T>(std::marker::PhantomData<T>);
struct LCons<T, Tail: SizedList<Elem = T>>(T, Tail);
impl<T> SizedList for LNil<T> {
type Elem = T;
const LEN: usize = 0;
fn to_vec(&self) -> std::vec::Vec<T>
where
T: Clone,
{
vec![]
}
}
impl<T, Tail: SizedList<Elem = T>> SizedList for LCons<T, Tail> {
type Elem = T;
const LEN: usize = 1 + Tail::LEN;
fn to_vec(&self) -> std::vec::Vec<T>
where
T: Clone,
{
let mut v = vec![self.0.clone()];
v.extend(self.1.to_vec());
v
}
}
impl<T, Tail: SizedList<Elem = T>> LCons<T, Tail> {
fn head(&self) -> &T {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_const_generic_head() {
let v = Vec2 { data: [1, 2, 3] };
assert_eq!(v.head(), 1);
}
#[test]
fn test_const_generic_map() {
let v = Vec2 { data: [1, 2, 3] };
let d = v.map(|x| x * 2);
assert_eq!(d.data, [2, 4, 6]);
}
#[test]
fn test_const_generic_zip() {
let a = Vec2 { data: [1, 2] };
let b = Vec2 { data: [10, 20] };
let z = zip_vec(&a, &b);
assert_eq!(z.data, [(1, 10), (2, 20)]);
}
#[test]
fn test_const_generic_replicate() {
let v: Vec2<i32, 3> = Vec2::replicate(42);
assert_eq!(v.data, [42, 42, 42]);
}
#[test]
fn test_peano_vec() {
let inner = vcons::<i32, Zero, VNil>(3, vnil());
let mid = vcons::<i32, Succ<Zero>, _>(2, inner);
let pv = vcons::<i32, Succ<Succ<Zero>>, _>(1, mid);
assert_eq!(*pv.head(), 1);
assert_eq!(pv.to_vec(), vec![1, 2, 3]);
}
#[test]
fn test_recursive_list() {
let l = LCons(10, LCons(20, LNil(std::marker::PhantomData)));
assert_eq!(*l.head(), 10);
assert_eq!(l.to_vec(), vec![10, 20]);
assert_eq!(<LCons<i32, LCons<i32, LNil<i32>>> as SizedList>::LEN, 2);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_const_generic_head() {
let v = Vec2 { data: [1, 2, 3] };
assert_eq!(v.head(), 1);
}
#[test]
fn test_const_generic_map() {
let v = Vec2 { data: [1, 2, 3] };
let d = v.map(|x| x * 2);
assert_eq!(d.data, [2, 4, 6]);
}
#[test]
fn test_const_generic_zip() {
let a = Vec2 { data: [1, 2] };
let b = Vec2 { data: [10, 20] };
let z = zip_vec(&a, &b);
assert_eq!(z.data, [(1, 10), (2, 20)]);
}
#[test]
fn test_const_generic_replicate() {
let v: Vec2<i32, 3> = Vec2::replicate(42);
assert_eq!(v.data, [42, 42, 42]);
}
#[test]
fn test_peano_vec() {
let inner = vcons::<i32, Zero, VNil>(3, vnil());
let mid = vcons::<i32, Succ<Zero>, _>(2, inner);
let pv = vcons::<i32, Succ<Succ<Zero>>, _>(1, mid);
assert_eq!(*pv.head(), 1);
assert_eq!(pv.to_vec(), vec![1, 2, 3]);
}
#[test]
fn test_recursive_list() {
let l = LCons(10, LCons(20, LNil(std::marker::PhantomData)));
assert_eq!(*l.head(), 10);
assert_eq!(l.to_vec(), vec![10, 20]);
assert_eq!(<LCons<i32, LCons<i32, LNil<i32>>> as SizedList>::LEN, 2);
}
}
Deep Comparison
Comparison: Example 178 — Length-Indexed Lists
Type Definition
OCaml
type zero = Zero
type 'n succ = Succ
type ('a, 'n) vec =
| Nil : ('a, zero) vec
| Cons : 'a * ('a, 'n) vec -> ('a, 'n succ) vec
Rust (const generics)
struct Vec2<T, const N: usize> {
data: [T; N],
}
Rust (Peano)
struct VNil;
struct VCons<T, N: Nat, Rest: TypeVec<T>>(T, Rest, PhantomData<N>);
Safe Head
OCaml
let head : type a n. (a, n succ) vec -> a = function
| Cons (x, _) -> x
Rust
impl<T: Copy, const N: usize> Vec2<T, N> {
fn head(&self) -> T { self.data[0] }
}
Zip (same length guaranteed)
OCaml
let rec zip : type a b n. (a, n) vec -> (b, n) vec -> (a * b, n) vec =
fun v1 v2 -> match v1, v2 with
| Nil, Nil -> Nil
| Cons (x, xs), Cons (y, ys) -> Cons ((x, y), zip xs ys)
Rust
fn zip_vec<T: Copy + Default, U: Copy + Default, const N: usize>(
a: &Vec2<T, N>, b: &Vec2<U, N>,
) -> Vec2<(T, U), N> {
let mut result = [(T::default(), U::default()); N];
for i in 0..N { result[i] = (a.data[i], b.data[i]); }
Vec2 { data: result }
}
Replicate
OCaml
let rec replicate : type a n. n length -> a -> (a, n) vec =
fun n x -> match n with
| LZ -> Nil
| LS n' -> Cons (x, replicate n' x)
Rust
impl<T: Default + Copy, const N: usize> Vec2<T, N> {
fn replicate(val: T) -> Self {
Vec2 { data: [val; N] }
}
}
Exercises
zip<T, U, const N: usize>(a: Vec2<T, N>, b: Vec2<U, N>) -> Vec2<(T,U), N> that combines element-wise.tail<T, const N: usize>(v: Vec2<T, N>) -> Vec2<T, {N-1}> (requires nightly or manual unsafe extraction).map<T, U, const N: usize>(v: Vec2<T, N>, f: impl Fn(T) -> U) -> Vec2<U, N>.