ExamplesBy LevelBy TopicLearning Paths
179 Expert

GADT Preventing Runtime Errors — Safe Head

Functional Programming

Tutorial

The Problem

head on an empty list is undefined. Instead of returning Option<T> and forcing callers to handle None, the typestate approach encodes emptiness in the type: SafeList<T, NonEmpty> has a head method; SafeList<T, Empty> does not. This converts a potential runtime panic into a compile error, and callers never need to unwrap. The pattern generalizes to any operation that requires a precondition: safe division, safe array access, safe database reads.

🎯 Learning Outcomes

  • • Encode list emptiness as a phantom type state (Empty vs. NonEmpty)
  • • Implement head only on SafeList<T, NonEmpty> — calling it on an empty list is a compile error
  • • See how push transitions from SafeList<T, Empty> to SafeList<T, NonEmpty>
  • • Understand this as a specialized typestate pattern (example 130) applied to data structures
  • Code Example

    struct SafeList<T, S> { data: Vec<T>, _state: PhantomData<S> }
    
    impl<T> SafeList<T, NonEmpty> {
        fn head(&self) -> &T { &self.data[0] }
    }
    // SafeList<T, Empty> has NO head method — compile error if you try

    Key Differences

  • Exhaustiveness: OCaml's GADT head on nonempty lists has no Nil case — it's impossible; Rust's impl SafeList<T, NonEmpty> block achieves the same by not defining the method on Empty.
  • Transition type: OCaml's Cons constructor changes the type index directly; Rust's push consumes the old value and returns a new type.
  • Runtime cost: Both are zero-cost — PhantomData/phantom types vanish at runtime.
  • Practical use: Rust's typestate is used in the rusqlite crate (transaction states), tokio (task states), and custom protocol implementations.
  • OCaml Approach

    OCaml's GADT approach is more elegant:

    type empty = Empty
    type nonempty = Nonempty
    type ('a, 's) safe_list =
      | Nil  : ('a, empty) safe_list
      | Cons : 'a * ('a, _) safe_list -> ('a, nonempty) safe_list
    let head : ('a, nonempty) safe_list -> 'a = function Cons (x, _) -> x
    

    Pattern matching on Nil in head is rejected by the compiler — OCaml's GADT exhaustiveness checker knows nonempty lists are never Nil.

    Full Source

    #![allow(clippy::all)]
    // Example 179: GADT Preventing Runtime Errors — Safe Head
    // Type-level guarantees that head can never be called on an empty list
    
    use std::marker::PhantomData;
    
    // === Approach 1: Phantom type states for emptiness ===
    
    struct Empty;
    struct NonEmpty;
    
    struct SafeList<T, S> {
        data: Vec<T>,
        _state: PhantomData<S>,
    }
    
    impl<T> SafeList<T, Empty> {
        fn new() -> Self {
            SafeList {
                data: Vec::new(),
                _state: PhantomData,
            }
        }
    
        // Pushing to empty list transitions to NonEmpty
        fn push(mut self, val: T) -> SafeList<T, NonEmpty> {
            self.data.push(val);
            SafeList {
                data: self.data,
                _state: PhantomData,
            }
        }
    }
    
    impl<T> SafeList<T, NonEmpty> {
        // head is ONLY available on NonEmpty lists
        fn head(&self) -> &T {
            &self.data[0]
        }
    
        fn tail(&self) -> &[T] {
            &self.data[1..]
        }
    
        fn push(mut self, val: T) -> SafeList<T, NonEmpty> {
            self.data.push(val);
            self
        }
    
        fn to_vec(&self) -> &Vec<T> {
            &self.data
        }
    }
    
    // Construct from a known non-empty source
    fn from_vec<T>(v: Vec<T>) -> Option<SafeList<T, NonEmpty>> {
        if v.is_empty() {
            None
        } else {
            Some(SafeList {
                data: v,
                _state: PhantomData,
            })
        }
    }
    
    // === Approach 2: NonEmpty struct (like OCaml NonEmpty module) ===
    
    #[derive(Debug, Clone)]
    struct NonEmptyVec<T> {
        head: T,
        tail: Vec<T>,
    }
    
    impl<T> NonEmptyVec<T> {
        fn new(head: T, tail: Vec<T>) -> Self {
            NonEmptyVec { head, tail }
        }
    
        fn from_vec(v: Vec<T>) -> Option<Self> {
            let mut iter = v.into_iter();
            iter.next().map(|head| NonEmptyVec {
                head,
                tail: iter.collect(),
            })
        }
    
        fn head(&self) -> &T {
            &self.head
        }
    
        fn tail(&self) -> &[T] {
            &self.tail
        }
    
        fn to_vec(&self) -> Vec<&T> {
            let mut v = vec![&self.head];
            v.extend(self.tail.iter());
            v
        }
    
        fn map<U>(&self, f: impl Fn(&T) -> U) -> NonEmptyVec<U> {
            NonEmptyVec {
                head: f(&self.head),
                tail: self.tail.iter().map(f).collect(),
            }
        }
    
        fn fold<U>(&self, init: U, f: impl Fn(U, &T) -> U) -> U {
            let acc = f(init, &self.head);
            self.tail.iter().fold(acc, f)
        }
    }
    
    // === Approach 3: Type-state with const generics ===
    // Using const generics to guarantee non-empty at type level
    
    struct StaticList<T, const N: usize> {
        data: [T; N],
    }
    
    // head only exists when N >= 1
    impl<T, const N: usize> StaticList<T, N> {
        fn head(&self) -> &T {
            &self.data[0]
        }
    
        fn len(&self) -> usize {
            N
        }
    }
    
    // Cannot create StaticList<T, 0> with this constructor
    fn static_list<T, const N: usize>(data: [T; N]) -> StaticList<T, N> {
        StaticList { data }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_safe_list_head() {
            let list = SafeList::<_, Empty>::new().push(42);
            assert_eq!(*list.head(), 42);
        }
    
        #[test]
        fn test_safe_list_chain() {
            let list = SafeList::<_, Empty>::new().push(1).push(2).push(3);
            assert_eq!(*list.head(), 1);
            assert_eq!(list.tail(), &[2, 3]);
        }
    
        #[test]
        fn test_from_vec() {
            assert!(from_vec::<i32>(vec![]).is_none());
            let l = from_vec(vec![1, 2]).unwrap();
            assert_eq!(*l.head(), 1);
        }
    
        #[test]
        fn test_nonempty_vec() {
            let ne = NonEmptyVec::new(1, vec![2, 3]);
            assert_eq!(*ne.head(), 1);
            assert_eq!(ne.tail(), &[2, 3]);
        }
    
        #[test]
        fn test_nonempty_map() {
            let ne = NonEmptyVec::new(1, vec![2, 3]);
            let d = ne.map(|x| x * 2);
            assert_eq!(*d.head(), 2);
            assert_eq!(d.tail, vec![4, 6]);
        }
    
        #[test]
        fn test_nonempty_fold() {
            let ne = NonEmptyVec::new(1, vec![2, 3]);
            assert_eq!(ne.fold(0, |a, x| a + x), 6);
        }
    
        #[test]
        fn test_nonempty_from_vec() {
            assert!(NonEmptyVec::<i32>::from_vec(vec![]).is_none());
            let ne = NonEmptyVec::from_vec(vec![42]).unwrap();
            assert_eq!(*ne.head(), 42);
        }
    
        #[test]
        fn test_static_list() {
            let sl = static_list([10, 20, 30]);
            assert_eq!(*sl.head(), 10);
            assert_eq!(sl.len(), 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_safe_list_head() {
            let list = SafeList::<_, Empty>::new().push(42);
            assert_eq!(*list.head(), 42);
        }
    
        #[test]
        fn test_safe_list_chain() {
            let list = SafeList::<_, Empty>::new().push(1).push(2).push(3);
            assert_eq!(*list.head(), 1);
            assert_eq!(list.tail(), &[2, 3]);
        }
    
        #[test]
        fn test_from_vec() {
            assert!(from_vec::<i32>(vec![]).is_none());
            let l = from_vec(vec![1, 2]).unwrap();
            assert_eq!(*l.head(), 1);
        }
    
        #[test]
        fn test_nonempty_vec() {
            let ne = NonEmptyVec::new(1, vec![2, 3]);
            assert_eq!(*ne.head(), 1);
            assert_eq!(ne.tail(), &[2, 3]);
        }
    
        #[test]
        fn test_nonempty_map() {
            let ne = NonEmptyVec::new(1, vec![2, 3]);
            let d = ne.map(|x| x * 2);
            assert_eq!(*d.head(), 2);
            assert_eq!(d.tail, vec![4, 6]);
        }
    
        #[test]
        fn test_nonempty_fold() {
            let ne = NonEmptyVec::new(1, vec![2, 3]);
            assert_eq!(ne.fold(0, |a, x| a + x), 6);
        }
    
        #[test]
        fn test_nonempty_from_vec() {
            assert!(NonEmptyVec::<i32>::from_vec(vec![]).is_none());
            let ne = NonEmptyVec::from_vec(vec![42]).unwrap();
            assert_eq!(*ne.head(), 42);
        }
    
        #[test]
        fn test_static_list() {
            let sl = static_list([10, 20, 30]);
            assert_eq!(*sl.head(), 10);
            assert_eq!(sl.len(), 3);
        }
    }

    Deep Comparison

    Comparison: Example 179 — Safe Head

    Non-Empty List Type

    OCaml

    type ('a, _) safe_list =
      | SNil  : ('a, empty) safe_list
      | SCons : 'a * ('a, _) safe_list -> ('a, nonempty) safe_list
    
    let safe_head : type a. (a, nonempty) safe_list -> a = function
      | SCons (x, _) -> x
    

    Rust

    struct SafeList<T, S> { data: Vec<T>, _state: PhantomData<S> }
    
    impl<T> SafeList<T, NonEmpty> {
        fn head(&self) -> &T { &self.data[0] }
    }
    // SafeList<T, Empty> has NO head method — compile error if you try
    

    NonEmpty Module/Struct

    OCaml

    module NonEmpty = struct
      type 'a t = { head : 'a; tail : 'a list }
      let head ne = ne.head
      let of_list = function [] -> None | x :: xs -> Some { head = x; tail = xs }
      let map f ne = { head = f ne.head; tail = List.map f ne.tail }
    end
    

    Rust

    struct NonEmptyVec<T> { head: T, tail: Vec<T> }
    
    impl<T> NonEmptyVec<T> {
        fn head(&self) -> &T { &self.head }
        fn from_vec(v: Vec<T>) -> Option<Self> {
            let mut iter = v.into_iter();
            iter.next().map(|head| NonEmptyVec { head, tail: iter.collect() })
        }
        fn map<U>(&self, f: impl Fn(&T) -> U) -> NonEmptyVec<U> {
            NonEmptyVec { head: f(&self.head), tail: self.tail.iter().map(f).collect() }
        }
    }
    

    Type-State Transition

    OCaml

    (* Constructing always gives nonempty *)
    let l = SCons (1, SCons (2, SNil))  (* type: (int, nonempty) safe_list *)
    

    Rust

    let list = SafeList::<_, Empty>::new()  // Empty
        .push(1)                             // → NonEmpty
        .push(2);                            // still NonEmpty
    // Type changes from SafeList<i32, Empty> to SafeList<i32, NonEmpty>
    

    Exercises

  • Add a tail(self) -> (T, SafeList<T, ...>) method that returns a SafeList<T, NonEmpty> if the original had 2+ elements, or SafeList<T, Empty> if exactly 1.
  • Implement first_two(&self) -> (&T, &T) only on lists with at least two elements — requiring a TwoOrMore phantom state.
  • Implement a safe_dequeue for a SafeQueue type with Enqueue and Dequeue operations that prevent dequeuing from an empty queue.
  • Open Source Repos