hey everyone π i've been meaning to write this post for a while. been doing a deep dive into how far you can push GADTs in Haskell and OCaml to simulate dependent type patterns without jumping all the way to Idris/Agda, and i have Thoughtsβ’
the classic example everyone starts with is a type-safe expression evaluator. in vanilla ADTs you get runtime type errors. with a GADT, the type checker itself enforces well-typedness:
{-# LANGUAGE GADTs #-} data Expr a where Lit :: Int -> Expr Int Bool :: Bool -> Expr Bool Add :: Expr Int -> Expr Int -> Expr Int If :: Expr Bool -> Expr a -> Expr a -> Expr a eval :: Expr a -> a eval (Lit n) = n eval (Bool b) = b eval (Add x y) = eval x + eval y eval (If c t e) = if eval c then eval t else eval e -- This won't compile β the type checker rejects it! -- bad = Add (Lit 3) (Bool True)
the key insight here β which i think a lot of people miss β is that Expr a is indexed by a *type*, and each constructor can specialize that index. this is what lets the compiler learn more type information when you descend into a pattern match branch.
same idea in OCaml (native since 4.00!):
type _ expr = | Lit : int -> int expr | Bool : bool -> bool expr | Add : int expr * int expr -> int expr | If : bool expr * 'a expr * 'a expr -> 'a expr let rec eval : type a. a expr -> a = function | Lit n -> n | Bool b -> b | Add (x, y) -> eval x + eval y | If (c,t,e) -> if eval c then eval t else eval e
note OCaml requires the type a. polymorphic recursion annotation β without it the type checker can't unify a across recursive calls. this trips up a lot of people new to OCaml GADTs.
so the thesis of this thread: GADTs let us encode some dependent type patterns at the cost of verbosity and manual singleton boilerplate. but where does the analogy break down? let's discuss π