ExamplesBy LevelBy TopicLearning Paths
176 Expert GitHub

Introduction to GADTs

Functional Programming

Tutorial

The Problem

Generalized Algebraic Data Types (GADTs) extend ordinary algebraic data types by allowing each constructor to specify a different return type for the type parameter. This enables type-safe expression trees where eval on int expr always returns int, not int | bool. GADTs were introduced in OCaml to bring the expressiveness of dependent types to mainstream functional programming. Rust simulates them via phantom types and sealed trait hierarchies.

🎯 Learning Outcomes

  • • Understand what GADTs are and why they extend ordinary ADTs
  • • Learn Rust's GADT simulation: phantom type parameters + sealed marker types
  • • See how the ExprType sealed trait restricts valid type indices
  • • Understand why Rust's simulation requires unreachable!() branches that OCaml's GADTs eliminate
  • Code Example

    use std::marker::PhantomData;
    
    enum ExprInner {
        Int(i64),
        Bool(bool),
        Add(Box<ExprInner>, Box<ExprInner>),
        If(Box<ExprInner>, Box<ExprInner>, Box<ExprInner>),
    }
    
    struct Expr<T> {
        inner: ExprInner,
        _phantom: PhantomData<T>,
    }

    Key Differences

  • Exhaustiveness: OCaml's GADT pattern matching is exhaustive per index — impossible cases are excluded; Rust's phantom type simulation requires unreachable!() for dead branches.
  • Constructor syntax: OCaml's : type expr syntax is built into the language; Rust requires phantom parameters and smart constructors.
  • Type variable scope: OCaml's type a. a expr -> a scopes the type variable per clause; Rust uses trait bounds and associated types.
  • Safety guarantee: Both prevent constructing ill-typed expressions at compile time; OCaml's guarantee is stronger (no runtime dead-code branches).
  • OCaml Approach

    OCaml's GADTs express this directly:

    type _ expr =
      | IntLit : int -> int expr
      | BoolLit : bool -> bool expr
      | Add : int expr * int expr -> int expr
      | Equal : int expr * int expr -> bool expr
    let rec eval : type a. a expr -> a = function
      | IntLit n -> n
      | BoolLit b -> b
      | Add (l, r) -> eval l + eval r
      | Equal (l, r) -> eval l = eval r
    

    The compiler knows that eval on int expr cannot match BoolLit — no unreachable!() needed.

    Full Source

    #![allow(clippy::all)]
    // Example 176: Introduction to GADTs
    // Rust doesn't have native GADTs, but we can simulate them with
    // PhantomData, traits, and sealed type-level markers.
    
    use std::marker::PhantomData;
    
    // === Approach 1: PhantomData + sealed traits to simulate GADTs ===
    
    mod sealed {
        pub trait ExprType {}
        impl ExprType for i64 {}
        impl ExprType for bool {}
    }
    
    #[derive(Debug)]
    enum ExprInner {
        Int(i64),
        Bool(bool),
        Add(Box<ExprInner>, Box<ExprInner>),
        If(Box<ExprInner>, Box<ExprInner>, Box<ExprInner>),
    }
    
    #[derive(Debug)]
    struct Expr<T: sealed::ExprType> {
        inner: ExprInner,
        _phantom: PhantomData<T>,
    }
    
    impl Expr<i64> {
        fn int(n: i64) -> Self {
            Expr {
                inner: ExprInner::Int(n),
                _phantom: PhantomData,
            }
        }
        fn add(a: Expr<i64>, b: Expr<i64>) -> Self {
            Expr {
                inner: ExprInner::Add(Box::new(a.inner), Box::new(b.inner)),
                _phantom: PhantomData,
            }
        }
        fn eval(&self) -> i64 {
            match &self.inner {
                ExprInner::Int(n) => *n,
                ExprInner::Add(a, b) => {
                    let a = Expr::<i64> {
                        inner: *a.clone(),
                        _phantom: PhantomData,
                    };
                    let b = Expr::<i64> {
                        inner: *b.clone(),
                        _phantom: PhantomData,
                    };
                    a.eval() + b.eval()
                }
                ExprInner::If(c, t, f) => {
                    let c = Expr::<bool> {
                        inner: *c.clone(),
                        _phantom: PhantomData,
                    };
                    let t = Expr::<i64> {
                        inner: *t.clone(),
                        _phantom: PhantomData,
                    };
                    let f = Expr::<i64> {
                        inner: *f.clone(),
                        _phantom: PhantomData,
                    };
                    if c.eval() {
                        t.eval()
                    } else {
                        f.eval()
                    }
                }
                _ => unreachable!(),
            }
        }
    }
    
    impl Expr<bool> {
        fn bool_val(b: bool) -> Self {
            Expr {
                inner: ExprInner::Bool(b),
                _phantom: PhantomData,
            }
        }
        fn eval(&self) -> bool {
            match &self.inner {
                ExprInner::Bool(b) => *b,
                _ => unreachable!(),
            }
        }
    }
    
    impl Clone for ExprInner {
        fn clone(&self) -> Self {
            match self {
                ExprInner::Int(n) => ExprInner::Int(*n),
                ExprInner::Bool(b) => ExprInner::Bool(*b),
                ExprInner::Add(a, b) => {
                    ExprInner::Add(Box::new((**a).clone()), Box::new((**b).clone()))
                }
                ExprInner::If(a, b, c) => ExprInner::If(
                    Box::new((**a).clone()),
                    Box::new((**b).clone()),
                    Box::new((**c).clone()),
                ),
            }
        }
    }
    
    fn if_expr(cond: Expr<bool>, then: Expr<i64>, else_: Expr<i64>) -> Expr<i64> {
        Expr {
            inner: ExprInner::If(
                Box::new(cond.inner),
                Box::new(then.inner),
                Box::new(else_.inner),
            ),
            _phantom: PhantomData,
        }
    }
    
    // === Approach 2: Enum-based with trait for evaluation ===
    
    trait Eval {
        type Output;
        fn eval(&self) -> Self::Output;
    }
    
    struct IntLit(i64);
    struct BoolLit(bool);
    struct AddExpr(Box<dyn Eval<Output = i64>>, Box<dyn Eval<Output = i64>>);
    
    impl Eval for IntLit {
        type Output = i64;
        fn eval(&self) -> i64 {
            self.0
        }
    }
    
    impl Eval for BoolLit {
        type Output = bool;
        fn eval(&self) -> bool {
            self.0
        }
    }
    
    impl Eval for AddExpr {
        type Output = i64;
        fn eval(&self) -> i64 {
            self.0.eval() + self.1.eval()
        }
    }
    
    // === Approach 3: Type-safe heterogeneous list with tuples ===
    
    // Rust's type system naturally supports this via nested tuples
    // (42, ("hello", (true, ())))  is the HList equivalent
    
    trait HList {}
    impl HList for () {}
    impl<H, T: HList> HList for (H, T) {}
    
    trait Head {
        type Item;
        fn head(&self) -> &Self::Item;
    }
    
    impl<H, T: HList> Head for (H, T) {
        type Item = H;
        fn head(&self) -> &H {
            &self.0
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_phantom_gadt_int() {
            assert_eq!(Expr::int(42).eval(), 42);
        }
    
        #[test]
        fn test_phantom_gadt_add() {
            assert_eq!(Expr::add(Expr::int(1), Expr::int(2)).eval(), 3);
        }
    
        #[test]
        fn test_phantom_gadt_bool() {
            assert_eq!(Expr::bool_val(true).eval(), true);
        }
    
        #[test]
        fn test_phantom_gadt_if() {
            let e = if_expr(Expr::bool_val(true), Expr::int(10), Expr::int(20));
            assert_eq!(e.eval(), 10);
            let e2 = if_expr(Expr::bool_val(false), Expr::int(10), Expr::int(20));
            assert_eq!(e2.eval(), 20);
        }
    
        #[test]
        fn test_trait_eval() {
            assert_eq!(IntLit(5).eval(), 5);
            assert_eq!(BoolLit(false).eval(), false);
            assert_eq!(AddExpr(Box::new(IntLit(3)), Box::new(IntLit(4))).eval(), 7);
        }
    
        #[test]
        fn test_hlist() {
            let hlist = (42, ("hello", (true, ())));
            assert_eq!(*hlist.head(), 42);
            assert_eq!(*hlist.1.head(), "hello");
            assert_eq!(*hlist.1 .1.head(), true);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_phantom_gadt_int() {
            assert_eq!(Expr::int(42).eval(), 42);
        }
    
        #[test]
        fn test_phantom_gadt_add() {
            assert_eq!(Expr::add(Expr::int(1), Expr::int(2)).eval(), 3);
        }
    
        #[test]
        fn test_phantom_gadt_bool() {
            assert_eq!(Expr::bool_val(true).eval(), true);
        }
    
        #[test]
        fn test_phantom_gadt_if() {
            let e = if_expr(Expr::bool_val(true), Expr::int(10), Expr::int(20));
            assert_eq!(e.eval(), 10);
            let e2 = if_expr(Expr::bool_val(false), Expr::int(10), Expr::int(20));
            assert_eq!(e2.eval(), 20);
        }
    
        #[test]
        fn test_trait_eval() {
            assert_eq!(IntLit(5).eval(), 5);
            assert_eq!(BoolLit(false).eval(), false);
            assert_eq!(AddExpr(Box::new(IntLit(3)), Box::new(IntLit(4))).eval(), 7);
        }
    
        #[test]
        fn test_hlist() {
            let hlist = (42, ("hello", (true, ())));
            assert_eq!(*hlist.head(), 42);
            assert_eq!(*hlist.1.head(), "hello");
            assert_eq!(*hlist.1 .1.head(), true);
        }
    }

    Deep Comparison

    Comparison: Example 176 — Introduction to GADTs

    GADT Type Definition

    OCaml

    type _ expr =
      | Int  : int  -> int expr
      | Bool : bool -> bool expr
      | Add  : int expr * int expr -> int expr
      | If   : bool expr * 'a expr * 'a expr -> 'a expr
    

    Rust

    use std::marker::PhantomData;
    
    enum ExprInner {
        Int(i64),
        Bool(bool),
        Add(Box<ExprInner>, Box<ExprInner>),
        If(Box<ExprInner>, Box<ExprInner>, Box<ExprInner>),
    }
    
    struct Expr<T> {
        inner: ExprInner,
        _phantom: PhantomData<T>,
    }
    

    Type-Safe Evaluation

    OCaml

    let rec eval : type a. a expr -> a = function
      | Int n -> n
      | Bool b -> b
      | Add (a, b) -> eval a + eval b
      | If (cond, t, f) -> if eval cond then eval t else eval f
    

    Rust

    impl Expr<i64> {
        fn eval(&self) -> i64 {
            match &self.inner {
                ExprInner::Int(n) => *n,
                ExprInner::Add(a, b) => { /* reconstruct typed wrappers */ }
                _ => unreachable!(),
            }
        }
    }
    
    impl Expr<bool> {
        fn eval(&self) -> bool {
            match &self.inner {
                ExprInner::Bool(b) => *b,
                _ => unreachable!(),
            }
        }
    }
    

    Trait-Based Alternative (Rust)

    Rust

    trait Eval {
        type Output;
        fn eval(&self) -> Self::Output;
    }
    
    struct IntLit(i64);
    impl Eval for IntLit {
        type Output = i64;
        fn eval(&self) -> i64 { self.0 }
    }
    
    struct AddExpr(Box<dyn Eval<Output = i64>>, Box<dyn Eval<Output = i64>>);
    impl Eval for AddExpr {
        type Output = i64;
        fn eval(&self) -> i64 { self.0.eval() + self.1.eval() }
    }
    

    Heterogeneous Lists

    OCaml

    type _ hlist =
      | HNil  : unit hlist
      | HCons : 'a * 'b hlist -> ('a * 'b) hlist
    
    let hd : type a b. (a * b) hlist -> a = function
      | HCons (x, _) -> x
    

    Rust

    trait HList {}
    impl HList for () {}
    impl<H, T: HList> HList for (H, T) {}
    
    trait Head {
        type Item;
        fn head(&self) -> &Self::Item;
    }
    
    impl<H, T: HList> Head for (H, T) {
        type Item = H;
        fn head(&self) -> &H { &self.0 }
    }
    
    // Usage: (42, ("hello", (true, ())))
    

    Exercises

  • Add a Neg : int expr -> int expr constructor and implement its evaluation.
  • Add IfThenElse : bool expr -> 'a expr -> 'a expr -> 'a expr — how does the type constraint on the two branches affect the Rust simulation?
  • Verify at compile time that add(bool_lit(true), int_lit(1)) is rejected.
  • Open Source Repos