Part 5 of Haskell from First Principles.
The snippets are independent examples. Later declarations of Point, Shape, or Student replace the earlier presentation. Maybe and Either are already in the Prelude; their declarations here explain their structure and should not be redefined alongside the Prelude versions.
Parts 1–4 established functions, types, pattern matching, and list recursion.
So far, however, the examples have mostly used built-in types such as Int, Bool, and [a].
Custom algebraic data types let us represent a problem directly in the type system.
This is one of the most important parts of Haskell. Once you understand algebraic data types (ADTs), you can represent trees, mathematical expressions, syntax trees, optional values, errors, and many other structures.
This article develops:
- The grammar of
datadeclarations. - The distinction between type constructors and data constructors.
- Sum types and product types.
- Parameterized types such as
Maybe a. - Pattern matching on custom types.
- Recursive data types, including binary trees.
- Record syntax.
- The distinction between
data,type, andnewtype.
A small arithmetic-expression interpreter brings these ideas together.
5.1 Why do we need to define our own types?
Suppose we want to represent three colors: red, green, and blue.
One possibility is to use integers:
red = 0
green = 1
blue = 2But this representation has a problem: what does 17 mean? Is it a valid color?
The type system does not enforce our intended restriction.
Instead, Haskell allows us to introduce a new type:
data Color = Red | Green | Blue
deriving (Eq, Show)Let us examine the syntax.
| Part | Role |
|---|---|
data | Introduces the declaration |
Color | Names the type constructor |
Red | Green | Blue | Lists the alternative data constructors |
The declaration says:
A value of type
Colorcan be constructed using exactly one of the three constructors:Red,Green, orBlue.
Now we can write:
favoriteColor :: Color
favoriteColor = BlueAnd functions can operate on the new type:
colorCode :: Color -> Int
colorCode Red = 1
colorCode Green = 2
colorCode Blue = 3For example:
colorCode Red -- 1
colorCode Blue -- 3Notice how pattern matching from Block 4 works naturally with this new type.
The final line of our type declaration,
deriving (Eq, Show)asks the compiler to automatically generate equality and string-representation functionality. Section 5.9 explains these derived instances.
5.2 Type constructors versus data constructors
This is the first important conceptual distinction.
Consider again:
data Color = Red | Green | BlueHere:
Coloris a type constructor.Red,Green, andBlueare data constructors.
They operate at different levels.
A type constructor forms a type. A data constructor produces a value belonging to a type.
For our example:
Red :: Color
Green :: Color
Blue :: ColorBut Color itself is not a value.
For example, this is invalid:
x = Colorbecause Color names a type rather than a data constructor.
Constructors can be functions
Consider a new declaration:
data Point = Point Double Double
deriving (Show, Eq)This introduces a type named Point and a data constructor also named Point.
The constructor has type:
Point :: Double -> Double -> PointThis looks exactly like a function type!
We can use it to create values:
p1 :: Point
p1 = Point 1.0 2.0
p2 :: Point
p2 = Point 3.0 4.0Notice that the same spelling Point appears in both roles, but Haskell distinguishes them by syntactic context and namespace.
The constructor Point takes two Double arguments and returns a value of type Point.
In fact, data constructors behave like functions when constructing values.
For example:
makePoint :: Double -> Double -> Point
makePoint = PointThis is valid because Point is already a constructor function of the required type.
A constructor can also be partially applied:
pointOnX1 :: Double -> Point
pointOnX1 = Point 1.0Then:
pointOnX1 5.0constructs:
Point 1.0 5.0Naming conventions
Haskell follows a naming convention that is also important to its grammar.
| Category | Examples | Naming rule |
|---|---|---|
| Type constructors | Int, Bool, Color, Maybe | Start uppercase |
| Data constructors | Red, Blue, Just, Nothing | Start uppercase |
| Variables/functions | x, square, colorCode | Start lowercase |
| Type variables | a, b, result | Start lowercase |
Symbolic constructors such as : are a special syntactic form.
5.3 Sum types: choosing between alternatives
The declaration:
data Color = Red | Green | Bluedefines a sum type.
Why is it called a sum?
Because a value of the type is selected from a collection of alternatives.
Suppose we define:
data Answer = Yes | No | Unknown
deriving (Eq, Show)Counting fully defined constructor values, there are three possibilities:
Now consider:
data Coin = Heads | Tails
deriving (Eq, Show)There are two possible values:
More generally, if types and are disjoint alternatives in a tagged union, then:
The symbol represents a choice between alternatives.
This is why these constructions are called sum types.
A more useful example: geometric shapes
Consider:
data Shape
= Circle Double
| Rectangle Double Double
deriving (Show)A value of type Shape is either:
- A circle, represented by its radius.
- A rectangle, represented by its width and height.
For example:
s1 = Circle 3.0
s2 = Rectangle 4.0 5.0Both values have type Shape.
Notice that constructors can carry additional data.
We can define:
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * hFor example:
area (Circle 2.0) -- approximately 12.566
area (Rectangle 3.0 4.0) -- 12.0Here, the pattern:
Circle rmatches a Circle value and extracts its radius.
Similarly:
Rectangle w hmatches a Rectangle value and extracts its width and height.
The constructors are doing two related but opposite jobs:
| Position | Example | Meaning |
|---|---|---|
| Expression | Circle 3.0 | Construct a value |
| Pattern | Circle r | Decompose a value |
This is the same construction/decomposition duality we saw with lists in Block 4.
5.4 Product types: combining multiple values
Now consider:
data Point = Point Double Double
deriving (Show)A point consists of two coordinates.
The geometric model is a Cartesian product:
The symbol represents a Cartesian product.
This is the idea behind a product type. Double represents coordinates with finite-precision floating-point values rather than exact mathematical real numbers.
For example:
data Student = Student String Int
deriving (Show)A Student value contains both a name and an age.
alice = Student "Alice" 20We can extract the fields by pattern matching:
studentName :: Student -> String
studentName (Student name age) = name
studentAge :: Student -> Int
studentAge (Student name age) = ageOr, more concisely:
studentName (Student name _) = name
studentAge (Student _ age) = ageRecall that _ is the wildcard pattern introduced in Block 4.
Why is it called a product?
Suppose a type has possible values and type has possible values.
Then a pair , where and , has:
possible values.
For example:
data BoolPair = BoolPair Bool Bool
deriving (Show)There are four possible values:
BoolPair False False
BoolPair False True
BoolPair True False
BoolPair True TrueBecause:
This explains the terminology. These cardinality equations count total values. Lazy Haskell also admits undefined computations and partially defined structures, which the simple arithmetic does not count.
Sum types versus product types
| Sum type | Product type |
|---|---|
| A or B | A and B |
| Choose one tagged alternative | Combine components in one constructor |
| Cardinalities add | Cardinalities multiply |
The mathematical correspondence is:
These are called algebraic data types because more complicated data types can be built by combining sums and products.
5.5 Combining sums and products
Let us revisit the earlier example:
data Shape
= Circle Double
| Rectangle Double DoubleThis is simultaneously a sum and a product construction.
A Shape is either a Circle containing one number, or a Rectangle containing two numbers.
Mathematically, its structure resembles:
The Circle and Rectangle constructor tags distinguish the two alternatives.
An important observation is that most interesting data types are combinations of sums and products.
For example, consider:
data Message
= TextMessage String
| NumberMessage Int
| PairMessage String IntThe mathematical structure is:
This algebraic viewpoint is helpful for both type design and program correctness.
5.6 Parameterized types: Maybe a
So far, our custom types have been concrete types.
But Haskell also supports type definitions with type parameters.
One particularly useful example is Maybe.
Its definition, in simplified form, is:
data Maybe a
= Nothing
| Just aMaybe is already provided by Haskell, so you don’t need to redefine it.
Let us analyze the syntax.
The type constructor Maybe takes a type parameter a.
For example:
Maybe Int
Maybe Bool
Maybe Stringare all types.
The constructors are:
Nothing :: Maybe a
Just :: a -> Maybe aThe Nothing constructor represents the absence of a value.
The Just constructor wraps an existing value.
Examples:
Just 3 :: Maybe Int
Just True :: Maybe Bool
Just "hello" :: Maybe String
Nothing :: Maybe aWhy is Maybe useful?
Recall our first function from Block 4:
first (x:xs) = xIt fails on the empty list.
Instead, we can define a function that explicitly represents the possibility of failure:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just xNow:
safeHead [1,2,3] -- Just 1
safeHead [] -- NothingNo runtime pattern-match failure is necessary.
Furthermore, the type itself communicates that the result might be absent:
safeHead :: [a] -> Maybe aThis is a very important feature of Haskell: the type system can explicitly represent different possible outcomes of a computation.
Pattern matching on Maybe
Suppose we want to use the result.
valueOrZero :: Maybe Int -> Int
valueOrZero Nothing = 0
valueOrZero (Just x) = xFor example:
valueOrZero Nothing -- 0
valueOrZero (Just 5) -- 5The function has to account for both constructors.
This is preferable to assuming that a value always exists.
A more general function
We can make the fallback value an argument:
valueOr :: a -> Maybe a -> a
valueOr fallback Nothing = fallback
valueOr fallback (Just x) = xFor example:
valueOr 0 Nothing -- 0
valueOr 0 (Just 5) -- 5
valueOr "unknown" Nothing -- "unknown"Notice the polymorphic type:
a -> Maybe a -> aWe can understand it using the rules from Block 2: the fallback value, the value wrapped by Just, and the result all have the same type a.
A mathematical interpretation
If a type has possible values, then Maybe A has possible values.
There is one additional possibility, Nothing.
Thus:
This is another example of a sum type.
5.7 Type constructors can take multiple type parameters
Consider a computation that may fail with an error message.
Maybe a can indicate failure:
Nothingbut it cannot explain the reason for failure.
The Prelude also provides Either; its simplified declaration is:
data Either a b
= Left a
| Right bThis type has two type parameters.
Its constructors are:
Left :: a -> Either a b
Right :: b -> Either a bThe type Either String Int, for example, can represent either a string error message or an integer result.
Right 42 :: Either String Int
Left "Invalid input" :: Either String IntA function can make this distinction explicit:
safeDivide :: Integer -> Integer -> Either String Integer
safeDivide x 0 = Left "Division by zero"
safeDivide x y = Right (x `div` y)Examples:
safeDivide 10 2
-- Right 5
safeDivide 10 0
-- Left "Division by zero"Notice that the return type is:
Either String Integerrather than simply:
IntegerThe function may produce either an error or a successful integer result.
This is a common way to represent errors. The example uses arbitrary-precision Integer, so the demonstration is about division by zero without adding bounded-Int overflow behavior.
Both types make the alternatives visible to callers; a caller can inspect them with pattern matching.
5.8 Recursive data types: binary trees
Now we arrive at one of the most important ideas in this block.
Recall from Block 4 that lists have a recursive structure:
Here, represents the single possibility of an empty list.
We can define other recursive structures in Haskell.
For example, consider a binary tree:
data Tree a
= Empty
| Node a (Tree a) (Tree a)
deriving (Show, Eq)Let us carefully analyze this declaration.
A tree is either:
Empty, an empty tree.Node x left right, containing a valuexand two subtrees.
The constructors have types:
Empty :: Tree a
Node :: a -> Tree a -> Tree a -> Tree aThe interesting part is that the type being defined, Tree a, appears inside its own definition.
This is a recursive data type.
Constructing a tree
For example:
tree :: Tree Int
tree =
Node 5
(Node 2 Empty Empty)
(Node 8 Empty Empty)This represents:
Node 5
/ \
Node 2 Node 8
/ \ / \
Empty Empty Empty EmptyThree Node constructors and four Empty constructors make this Tree Int.
Pattern matching on trees
We can define the number of nodes in a tree:
size :: Tree a -> Int
size Empty = 0
size (Node x left right) =
1 + size left + size rightFor our example:
size tree -- 3This definition is directly analogous to the recursive list-length function from Block 4.
The structure of the data determines the structure of the recursion.
For an empty tree:
For a node:
Summing the values in a tree
We can similarly write:
sumTree :: Num a => Tree a -> a
sumTree Empty = 0
sumTree (Node x left right) =
x + sumTree left + sumTree rightFor our tree:
sumTree tree -- 15because:
Mapping a function over a tree
Now consider a more general operation:
mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f Empty = Empty
mapTree f (Node x left right) =
Node (f x) (mapTree f left) (mapTree f right)This function applies f to every value in the tree while preserving the tree’s structure.
For example:
mapTree (+1) treeproduces:
Node 6
(Node 3 Empty Empty)
(Node 9 Empty Empty)The type:
(a -> b) -> Tree a -> Tree bshows that mapTree can transform the element type.
For example, it could map integers to strings:
mapTree show treeThis transforms a Tree Int into a Tree String.
Part 8 connects this pattern to Functor.
Recursive types and mathematical induction
The recursive definition of Tree suggests a natural proof principle.
To prove a property for every finite tree , we can:
- Prove .
- Assume and , then prove .
This is called structural induction.
It is closely related to the structure of recursive functions over trees.
5.9 Automatically deriving type-class instances
You may have noticed that our examples often contain:
deriving (Show, Eq)What does this mean?
Recall from Block 2 that type classes describe available operations.
For example:
(==) :: Eq a => a -> a -> BoolEquality comparison is available for types with an Eq instance.
Similarly:
show :: Show a => a -> Stringconverts a value into a string representation, provided its type has a Show instance.
By default, declaring a new data type does not automatically provide all possible operations.
But Haskell can derive certain instances.
Consider:
data Color = Red | Green | Blue
deriving (Eq, Show)Now these expressions work:
Red == Blue -- False
Red == Red -- True
show Green -- "Green"The compiler generates implementations based on the constructors.
For equality, different constructors compare unequal. Values built with the same constructor compare their corresponding fields, provided those field types also support equality.
For Show, the compiler generates a textual representation reflecting the constructor structure.
This is convenient, but only certain type classes support automatic deriving in ordinary Haskell. Part 7 studies type classes and instances.
5.10 Record syntax: naming constructor fields
Consider:
data Student = Student String Int
deriving (Show)This works, but it isn’t always easy to remember which argument means what.
Is the first argument the name or the age?
Haskell provides record syntax:
data Student = Student
{ studentName :: String
, studentAge :: Int
}
deriving (Show)Now we can construct a student using named fields:
alice :: Student
alice = Student
{ studentName = "Alice"
, studentAge = 20
}The compiler automatically creates field accessor functions:
studentName :: Student -> String
studentAge :: Student -> IntFor example:
studentName alice -- "Alice"
studentAge alice -- 20We can also construct the same value positionally:
bob = Student "Bob" 22Record syntax names fields of an ordinary algebraic data type. For this single-constructor Student, both selectors cover every fully constructed value. If a field appears on only some constructors of a sum type, its selector is partial: applying it to another constructor raises an error. Pattern matching can make that missing-field case explicit.
Record updates
Suppose Alice becomes 21 years old.
We can write:
olderAlice = alice { studentAge = 21 }The result is a new Student value with the updated age.
It does not mutate the original alice.
For example:
studentAge alice -- 20
studentAge olderAlice -- 21This is important: record update syntax creates a new value rather than modifying an existing one.
5.11 data versus type versus newtype
Haskell provides three closely related constructs:
data
type
newtypeThey have different meanings.
data: introduce a new algebraic data type
We already know this one:
data Point = Point Double Double
deriving (Show)This introduces a new type called Point, with a constructor that holds two Double values.
A value of type Point is distinct from an ordinary pair of Double values.
type: create a type synonym
Consider:
type Name = StringThis introduces a new name for an existing type.
For example:
greet :: Name -> String
greet name = "Hello, " ++ nameBut Name is not a genuinely new type.
It is simply another name for String.
Therefore:
greet "Alice"works directly.
We can also define:
type Coordinate = (Double, Double)Here, the type (Double, Double) is a tuple type.
The declaration gives it a more descriptive name.
newtype: introduce a distinct wrapper type
Now suppose we want to represent a user ID.
We could write:
type UserId = IntBut this would make UserId interchangeable with Int, which may be undesirable.
Instead, we can write:
newtype UserId = UserId Int
deriving (Eq, Show)Now:
user1 :: UserId
user1 = UserId 123The type UserId is distinct from Int.
For example:
getUserId :: UserId -> Int
getUserId (UserId n) = nYou cannot generally pass an Int where a UserId is expected without explicitly constructing a UserId.
Unlike an ordinary data declaration, newtype has exactly one constructor and one field, with the same runtime representation as its underlying type. This also changes behavior around undefined values: UserId undefined is itself undefined, while a lazy data wrapper can expose its constructor around an undefined field. Thus newtype is not semantically interchangeable with data in every lazy program. See the Haskell Report on datatype renamings.
Comparison
| Construct | Creates a distinct type? | Main purpose |
|---|---|---|
type | No | Give an existing type another name |
newtype | Yes | Wrap one value in a distinct type |
data | Yes | Define a general algebraic data type |
Use data for general alternatives and multiple fields; use newtype when a distinct single-field wrapper expresses the intended model.
5.12 Application: representing mathematical expressions
Let us now apply what we have learned to a concrete example.
Suppose we want to represent arithmetic expressions such as:
Rather than directly calculating the result, we first want to represent the expression as a data structure.
We can define:
data Expr
= Const Int
| Add Expr Expr
| Multiply Expr Expr
deriving (Show, Eq)This says that an expression is one of three possibilities:
- A constant integer.
- An addition of two expressions.
- A multiplication of two expressions.
Notice the recursive structure.
Constructing an expression
The mathematical expression:
can be represented as:
expr :: Expr
expr =
Multiply
(Add (Const 2) (Const 3))
(Const 4)Visually:
Multiply
/ \
Add Const 4
/ \
Const 2 Const 3This structure is called an abstract syntax tree (AST).
Compilers and interpreters frequently use such structures.
Writing an evaluator
Now let’s define an evaluator:
eval :: Expr -> Int
eval (Const n) = n
eval (Add x y) =
eval x + eval y
eval (Multiply x y) =
eval x * eval yFor example:
eval exprreturns:
20Let us derive this.
Notice that the evaluator follows the recursive structure of the expression type exactly.
Each constructor has a corresponding equation.
Why is this example important?
It demonstrates a powerful pattern:
- Define a recursive algebraic data type to represent a language.
- Construct values representing expressions in that language.
- Write recursive functions that interpret or transform those expressions.
For example, we could extend Expr with subtraction, division, variables, or function applications.
This is the foundation of interpreters, compilers, symbolic mathematics, and domain-specific languages.
It also illustrates an important philosophical point about Haskell: a program can itself be represented as a value, and another program can manipulate that value through ordinary functions.
5.13 The grammar of algebraic data types
Let us now summarize the new syntax formally.
A simplified Haskell data declaration has the structure:
DataDeclaration :=
data TypeConstructor TypeVariable*
= Constructor FieldType*
| Constructor FieldType*
| ...For example:
data Color = Red | Green | Blueor:
data Maybe a = Nothing | Just aor:
data Tree a
= Empty
| Node a (Tree a) (Tree a)In a prefix constructor declaration, parentheses group the applied type Tree a as one field. Without them, the constructor-field grammar would treat the tokens as separate field types. This is a grouping rule for declarations; in a signature such as Tree a -> Int, type application already binds more tightly than the arrow.
Type constructor application
In:
Maybe IntMaybe is a type constructor applied to the type Int.
Similarly:
Tree Boolis a type obtained by applying Tree to Bool.
This looks similar to ordinary function application:
f xBut type constructors operate at the type level, while ordinary functions operate at the value level.
A kind classifies a type constructor: for example, Maybe needs one type argument to form a concrete element type.
Data constructors and their signatures
Given:
data Tree a
= Empty
| Node a (Tree a) (Tree a)the constructors have types:
Empty :: Tree a
Node :: a -> Tree a -> Tree a -> Tree aNotice that a constructor can have zero or more arguments.
Every constructor produces a value of the declared type.
This also explains why the following would not be a valid constructor definition:
-- Invalid
data Strange = MakeStrange Int -> BoolAfter a constructor name, ordinary field types are listed; we cannot write a function arrow there to describe the constructor’s result. The result is determined by the data declaration itself.
If we wanted a constructor holding a function, we would write:
data Strange = MakeStrange (Int -> Bool)Then:
MakeStrange :: (Int -> Bool) -> StrangeThis distinction between a field that is a function and the function type of a constructor is worth understanding.
5.14 Exercises
These exercises are designed to test your understanding of constructors, pattern matching, and recursive types.
Try each exercise before opening its solution. The undefined expressions in skeletons mark code to replace; evaluating an unfinished skeleton raises an error.
Exercise 5.1
Give the types of both constructors.
data Result a
= Failure String
| Success aSolution
Failure :: String -> Result a
Success :: a -> Result aEvery constructor returns Result a; its fields determine its argument types.
Exercise 5.2
Convert Maybe Int to Int, returning -1 for Nothing.
getValue :: Maybe Int -> IntSolution
getValue Nothing = -1
getValue (Just x) = xMatch both constructors, extracting the value stored in Just.
Exercise 5.3
Count the leaves in this alternative tree type.
data LeafTree a
= Leaf a
| Branch (LeafTree a) (LeafTree a)
countLeaves :: LeafTree a -> IntSolution
countLeaves (Leaf _) = 1
countLeaves (Branch l r) =
countLeaves l + countLeaves rA Leaf contributes one. A Branch contributes the sum for its two subtrees. The different name LeafTree keeps this exercise distinct from the earlier Tree declaration.
Exercise 5.4
Predict the value and infer the type of swap.
data Pair a b = Pair a b
deriving (Show)
swap (Pair x y) = Pair y xAfter loading those definitions, evaluate:
ghci> swap (Pair 3 True)Solution
swap :: Pair a b -> Pair b aghci> swap (Pair 3 True)
Pair True 3Pattern matching extracts x = 3 and y = True; construction places them in the opposite order.
Exercise 5.5
Compute the height of the binary tree from Section 5.8. Count an empty tree as height 0.
height :: Tree a -> IntSolution
height Empty = 0
height (Node _ l r) =
1 + max (height l) (height r)A nonempty tree has height one plus the larger subtree height. Thus a singleton node has height 1.
5.15 Summary
The main constructs have distinct roles:
| Concept | Haskell syntax | Meaning |
|---|---|---|
| Define a type | data Color = Red | Blue | Introduce a new algebraic data type |
| Sum type | A | B | Choose one constructor |
| Product type | Point Double Double | Combine multiple fields |
| Parameterized type | Maybe a | Type constructor accepting a type parameter |
| Data constructor | Just 3 | Construct a value |
| Pattern matching | Just x | Decompose a value |
| Recursive type | Tree a | A data type defined in terms of itself |
| Record | { name :: String } | Give names to fields |
| Type alias | type Name = String | Give an existing type another name |
| Newtype | newtype Id = Id Int | Introduce a distinct single-field wrapper type |
Data types determine computation structure
Consider the definition:
data Tree a
= Empty
| Node a (Tree a) (Tree a)Its mathematical structure is:
This single equation already tells us how to write many recursive algorithms.
For example, if we want to reduce a tree to a value of type , we can specify:
- A value for the empty tree.
- A combining function for each nonempty node.
Then define:
This pattern describes size, sumTree, and many other tree algorithms.
We could even implement a general tree fold:
foldTree :: b -> (a -> b -> b -> b) -> Tree a -> b
foldTree z g Empty = z
foldTree z g (Node x left right) =
g x (foldTree z g left) (foldTree z g right)Then:
size = foldTree 0 (\_ l r -> 1 + l + r)and:
sumTree = foldTree 0 (\x l r -> x + l + r)Here \ introduces an anonymous function. Part 6 develops lambdas and higher-order functions; the important point here is the recursion pattern shared by these definitions.
The important observation is that a recursive data type determines a general pattern of recursion over values of that type.
This is a deep connection between algebraic data types, recursion, and mathematical induction. It also motivates the structure-preserving transformations in Part 8.
← Part 4 · Series index · Part 6 →
Adapted from Block 5 of the GPT-produced Haskell tutorial, with editorial and technical corrections.