100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Algebraic Data Types

How Haskell builds custom types from sum types and product types using data declarations, pattern matching, records, and recursion.

Type SystemBeginner10 min readJul 10, 2026
Analogies

Sum Types and Product Types

An algebraic data type is called 'algebraic' because it's built from two combining operations: sum types, which offer a choice between alternatives using |, and product types, which bundle several values together at once. data Shape = Circle Double | Rectangle Double Double is a sum type with two constructors -- any Shape value is either a Circle (carrying one Double radius) or a Rectangle (carrying two Double dimensions), never both and never neither. Rectangle itself is a small product type, since a Rectangle value always bundles exactly two Doubles together; the number of possible values of a sum type is the sum of each alternative's possibilities, and the number of possible values of a product type is the product of its fields' possibilities, which is exactly where the name comes from.

🏏

Cricket analogy: A dismissal type -- bowled, caught, LBW, run out -- is a sum type where a wicket is exactly one of those alternatives, never a blend, mirroring how a Shape value is either a Circle or a Rectangle, never both at once.

Pattern Matching on Constructors

Because a value's constructor tells you exactly which alternative of a sum type you're holding, functions consume ADTs by pattern matching on those constructors, either in equations or a case expression: area (Circle r) = pi * r * r; area (Rectangle w h) = w * h. GHC's exhaustiveness checker (enabled via -Wincomplete-patterns or -Wall) warns at compile time if a function forgets to handle one of a sum type's constructors, turning what would be a runtime crash in many other languages -- an unhandled case falling through to null or an exception -- into a compile-time warning you can act on before shipping.

🏏

Cricket analogy: An umpire's decision tree for a dismissal -- 'if bowled, out; if caught, out; if LBW, check height and impact' -- must cover every dismissal type or a real appeal gets missed, mirroring how area must pattern-match every Shape constructor or GHC warns of incomplete coverage.

haskell
data Shape
  = Circle Double
  | Rectangle Double Double
  deriving Show

area :: Shape -> Double
area (Circle r)      = pi * r * r
area (Rectangle w h) = w * h

-- Record syntax: named fields generate accessor functions
data Person = Person
  { name :: String
  , age  :: Int
  } deriving Show

birthday :: Person -> Person
birthday p = p { age = age p + 1 }   -- record update syntax

-- Recursive ADT: a binary search tree
data Tree a = Leaf | Node (Tree a) a (Tree a)

insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node Leaf x Leaf
insert x (Node l y r)
  | x < y     = Node (insert x l) y r
  | x > y     = Node l y (insert x r)
  | otherwise = Node l y r

main :: IO ()
main = do
  print (area (Circle 2.0))                -- 12.566...
  let alice = Person { name = "Alice", age = 29 }
  print (birthday alice)                   -- Person {name = "Alice", age = 30}

Record Syntax

Record syntax lets you name a constructor's fields instead of relying on position: data Person = Person { name :: String, age :: Int } simultaneously defines the Person constructor and two accessor functions, name :: Person -> String and age :: Person -> Int, generated automatically. Record update syntax, p { age = age p + 1 }, creates a new Person value with the age field changed and every other field copied unchanged, which is essential in a language where data is immutable and you can never mutate the original p in place.

🏏

Cricket analogy: A player's profile card listing 'name', 'battingAverage', and 'strikeRate' as named fields you can look up directly, rather than remembering that field two is always the average, mirrors how record syntax gives Person's age field a name instead of a bare position.

Recursive Data Types

An ADT constructor can reference the type it's defining, producing recursive structures: data Tree a = Leaf | Node (Tree a) a (Tree a) defines a binary tree where every Node holds a left subtree, a value, and a right subtree, and a tree eventually bottoms out at Leaf. Functions over recursive ADTs are naturally written recursively too, mirroring the data's own shape -- insert calls itself on the left or right subtree depending on the comparison, terminating exactly when it reaches a Leaf, which is also how Haskell's own built-in list type [a] is defined under the hood, as data [a] = [] | a : [a].

🏏

Cricket analogy: A knockout tournament bracket is recursively structured: each round's winner feeds into the next round exactly like Node (Tree a) a (Tree a) nests smaller trees inside larger ones, bottoming out at individual first-round matches, the equivalent of a Leaf.

Compile with -Wall (or at minimum -Wincomplete-patterns) to have GHC warn whenever a function's pattern match doesn't cover every constructor of a sum type -- this single flag catches an entire category of 'forgot to handle a case' bugs that would otherwise only surface as a runtime Non-exhaustive patterns error.

Before the DuplicateRecordFields extension, two constructors in the same module cannot reuse the same field name -- data Shape = Circle { radius :: Double } | Square { side :: Double } is fine, but giving both constructors a field literally named size is a compile error, because record syntax generates a single top-level accessor function per field name.

  • Algebraic data types combine sum types (|, a choice of alternatives) and product types (bundled fields) to model data precisely.
  • Pattern matching destructures a value based on which constructor it holds, and GHC's exhaustiveness checker (-Wincomplete-patterns) flags unhandled constructors at compile time.
  • Record syntax names a constructor's fields and auto-generates accessor functions; record update syntax p { field = newVal } creates a new, updated copy without mutation.
  • Recursive ADTs like data Tree a = Leaf | Node (Tree a) a (Tree a) let a type reference itself, modeling structures like trees and, in fact, Haskell's own list type.
  • Functions over recursive ADTs are naturally written recursively themselves, mirroring the shape of the data they consume.
  • Compiling with -Wall catches incomplete pattern matches before they become runtime Non-exhaustive patterns crashes.
  • Without DuplicateRecordFields, two constructors in the same module cannot share an identical field name.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#HaskellStudyNotes#AlgebraicDataTypes#Algebraic#Data#Types#Sum#StudyNotes#SkillVeris#ExamPrep