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.
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
-Wallcatches incomplete pattern matches before they become runtimeNon-exhaustive patternscrashes. - Without
DuplicateRecordFields, two constructors in the same module cannot share an identical field name.
Practice what you learned
1. In `data Shape = Circle Double | Rectangle Double Double`, what kind of type is `Shape` as a whole?
2. What does GHC's exhaustiveness checker (enabled via -Wincomplete-patterns or -Wall) do?
3. What does record update syntax like `p { age = age p + 1 }` actually do?
4. How is Haskell's own built-in list type conceptually defined?
5. Without the DuplicateRecordFields extension, what happens if two constructors in the same module both declare a field named `size`?
Was this page helpful?
You May Also Like
The Haskell Type System
How Haskell's static, strong type system uses type signatures, inference, and kinds to catch errors before a program ever runs.
Maybe and Either
How Haskell uses the Maybe and Either types to represent optional values and recoverable errors explicitly, without null or exceptions.
Type Classes Explained
How Haskell's type classes define shared interfaces like Eq, Ord, and Show, and how instances and constraints let ad-hoc polymorphism work safely.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics