Practical patterns for using Generalized Algebraic Data Types (GADTs) in
Turmeric. All examples require -Xgadt unless noted.
The classic GADT motivation: an expression tree whose type parameter tracks the result type, so the interpreter is total and type-safe.
; A two-type expression language: integers and booleans.
(defgadt Expr [a]
(IntLit int : (Expr int))
(BoolLit bool : (Expr bool))
(Add (Expr int) (Expr int) : (Expr int))
(IsZero (Expr int) : (Expr bool)))
; The interpreter returns int -- the type parameter 'a' is refined per arm.
(defn eval-expr [e] :int
(match e
(IntLit n) n
(BoolLit b) (if b 1 0)
(Add l r) (+ (eval-expr l) (eval-expr r))
(IsZero n) (if (= (eval-expr n) 0) 1 0)))
(defn main [] :int
; (2 + (IsZero 0)) -- only valid at type (Expr ???) mismatch, caught statically
(println (eval-expr (IntLit 42))) ; 42
(println (eval-expr (Add (IntLit 3) (IntLit 4)))) ; 7
(println (eval-expr (IsZero (IntLit 0)))) ; 1
(println (eval-expr (IsZero (Add (IntLit 1) (IntLit 0))))) ; 0
0)
What makes this a GADT: Add returns (Expr int) and IsZero returns
(Expr bool). In the IsZero arm the elaborator knows a = bool, so the
return type is refined without a cast. In the IntLit arm it knows a = int.
Without GADTs you would need a runtime option or either to represent
the two result types, plus an unsafe cast or pattern match to retrieve the
value.
Use a type-level natural number to track vector length at compile time. Safe
head and vzip-with become expressible without option.
; Type-level naturals
(defgadt Nat []
(Zero : Nat)
(Succ : Nat -> Nat))
; Length-indexed int vector: (Vec n) has exactly n elements.
; (Uses int elements; polymorphic element type requires HKT --
; see stdlib/gadt-vec.tur for a working implementation.)
(defgadt Vec [n]
(VNil : (Vec Zero))
(VCons : int -> (Vec n) -> (Vec (Succ n))))
; Length is computable at compile time -- no bounds check needed.
(defn vec-len [v] :int
(match v
(VNil) 0
(VCons _ tl) (+ 1 (vec-len tl))))
; Head is only callable on non-empty vectors.
; The return type is int, not (option int).
(defn vec-head [v] :int
(match v
(VCons x _) x))
; Only vectors of the same length can be zipped -- the type guarantees it.
(defn vzip-add [xs ys] :int
(match xs
(VNil) 0
(VCons x xtl)
(match ys
(VCons y ytl) (+ (* x y) (vzip-add xtl ytl)))))
(defn main [] :int
(let [v (VCons 1 (VCons 2 (VCons 3 (VNil))))]
(println (vec-len v)) ; 3
(println (vec-head v)) ; 1
(println (vzip-add v v))) ; 1*1 + 2*2 + 3*3 = 14
0)
Stdlib: stdlib/gadt-vec.tur provides vec-nil, vec-cons, vec-len,
vec-sum, vec-head-or, vec-tail, vmap, and vzip-with as a reusable
module. Import it with (load "stdlib/gadt-vec.tur").
Encode the expected argument list in the type so the wrong number or type of arguments is a compile-time error.
; A format descriptor -- 'args' is a phantom type tracking argument types.
; This is a simplified version: real type-level lists need HKT.
; Here we show the two-argument case explicitly.
(defgadt Fmt2 [a b]
; format string expecting (int, int)
(FIntInt : (Fmt2 int int))
; format string expecting (int, bool)
(FIntBool : (Fmt2 int bool)))
; A printf that is safe for exactly the arguments the format specifies.
(defn sprintf2 [fmt a b] :int
(match fmt
(FIntInt) (do (println a) (println b) 0)
(FIntBool) (do (println a) (if b (println 1) (println 0)) 0)))
(defn main [] :int
; Both arguments are correct -- compiles fine.
(sprintf2 (FIntInt) 42 99)
(sprintf2 (FIntBool) 10 true)
0)
Practical note: A full type-safe printf encoding the full argument list
(cons int (cons bool nil)) requires higher-kinded type-level lists, which
are deferred pending the HKT phase. The pattern above shows the core idea
with fixed arity.
The built-in Equal GADT lets you carry proofs of type equality across
function boundaries. Use coerce to convert values across proven equalities.
; Equal is built-in; you can also declare it yourself:
; (defgadt Equal [a b]
; (Refl : (Equal a a)))
; Symmetry: (Equal a b) -> (Equal b a)
; In the Refl arm a = b, so Refl also has type (Equal b a).
(defn sym [eq] :(Equal b a)
(match eq
(Refl) (Refl)))
; Transitivity: (Equal a b) -> (Equal b c) -> (Equal a c)
; Both Refl arms unify a=b and b=c, giving a=c.
(defn trans [ab bc] :(Equal a c)
(match ab
(Refl)
(match bc
(Refl) (Refl))))
; coerce uses an equality proof to safely reinterpret a value.
; No cast instruction is emitted -- it is zero-overhead.
(defn use-eq [eq x] :int
(match eq
(Refl)
; In this arm a = int (from the Refl refinement)
; so x : a is the same as x : int
(+ x 1)))
(defn main [] :int
; Construct a proof that int = int
(let [proof (Refl)]
(println (use-eq proof 41)) ; 42
(println (coerce proof 100))) ; 100 -- zero overhead
0)
Equal to implement a typed heterogeneous container; A box that remembers the type of its contents via a witness.
(defgadt TypeBox [a]
(MkBox : int -> (TypeBox int))
(MkBoolBox : bool -> (TypeBox bool)))
; Open a TypeBox knowing it holds an int.
(defn open-int-box [box eq] :int
(match box
(MkBox n)
(match eq
; In this arm a = int, so coerce is safe
(Refl) (coerce eq n))
(MkBoolBox _) -1))
(defn main [] :int
(let [b (MkBox 99)]
(println (open-int-box b (Refl)))) ; 99
0)
GADTs and union types (-Xgadt -Xunion-types) can be combined. A function
can accept either a GADT value or a plain value through a union type:
; A typed tag for int-or-bool dispatch
(defgadt Tag [a]
(IntTag : (Tag int))
(BoolTag : (Tag bool)))
; default-value uses the tag to produce a zero value.
(defn default-value [t] :int
(match t
(IntTag) 0
(BoolTag) 0))
; A function accepting either an Int tag or a plain int through a union.
(defn accept-either [x : ((Tag int) | int)] :int
(match x
(v : (Tag int)) (default-value v)
(n : int) n))
(defn main [] :int
(println (accept-either (IntTag))) ; 0
(println (accept-either 42)) ; 42 -- requires -Xunion-types
0)
docs/gadts-guide.md -- Full language reference for GADTs and union typesdocs/gadts-plan.md -- Implementation plan and phase historystdlib/gadt-vec.tur -- Production-quality length-indexed vector stdlibstdlib/equal.tur -- Equal witness, sym, trans, substtests/fixtures/gadt-*/ -- All GADT test fixturestests/fixtures/union-types-*/ -- Union type and gradual typing fixtures