Types: Sets in Disguise
Types aren't red-tape the compiler makes you deal with — they're the domains and codomains from Chapter 3, made explicit and checked for you.
A type is just a set
Chapter 3 talked about domains and codomains as sets: the set of all Fruit, the set of all Colour. A type, in Haskell, is exactly this: a named set of values. Bool is the set . Int is (approximately) the set of machine-representable integers. When we write
x :: Int
we’re making the claim ” is an element of the set Int” — the same relationship as in ordinary math, just spelled :: instead of .
Figure: Every well-typed expression has exactly one type, and that type is decided before the program ever runs — the defining feature of static typing.
Building your own types
The built-in types (Int, Bool, Char, Double, …) are just a starting vocabulary. Haskell’s data keyword lets you declare new sets directly:
data Colour = Red | Yellow | Purple | Green
deriving (Show, Eq)
This declares Colour to be the four-element set — nothing more, nothing less. The compiler now knows, exhaustively, every possible value of type Colour, which is what lets it check whether a case expression covers every case.
Types can also carry data, which mathematically makes them products or sums of other sets:
-- a product type: Point holds BOTH a Double AND a Double
data Point = Point Double Double
-- a sum type: a Shape is EITHER a Circle OR a Rectangle
data Shape
= Circle Point Double -- centre, radius
| Rectangle Point Point -- two corners
Shape is the set-theoretic disjoint union of “all possible circles” and “all possible rectangles” — precisely mirroring how mathematicians build bigger sets out of smaller ones.
This correspondence has a name: the Curry–Howard correspondence, sometimes phrased as “propositions as types.” A product type (Point) corresponds to a logical AND; a sum type (Shape) corresponds to a logical OR; a function type a -> b corresponds to logical implication . A well-typed Haskell program is, quite literally, a proof of the proposition its type describes.
Polymorphism: functions over any set
Chapter 3’s colourOf worked on one specific domain. Often you want a function that works the same way regardless of which set is involved — this is parametric polymorphism:
identity :: a -> a
identity x = x
fst' :: (a, b) -> a
fst' (x, _) = x
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xs
Here a and b are type variables — placeholders that can be filled with any concrete type. length' doesn’t know or care whether it’s counting Fruit, Colour, or Int; the shape of a list is the same regardless of what’s inside it. This is a much stronger guarantee than it looks: a function with type a -> a genuinely can’t do anything to its argument except return it unchanged — there’s no way to inspect a value you know nothing about, so the type alone proves the function’s behaviour.
Typeclasses: constrained polymorphism
Sometimes you want some structure, without pinning down the exact type. A typeclass is a named set of operations a type can promise to support:
class Eq a where
(==) :: a -> a -> Bool
instance Eq Colour where
Red == Red = True
Yellow == Yellow = True
Purple == Purple = True
Green == Green = True
_ == _ = False
allSame :: Eq a => [a] -> Bool
allSame [] = True
allSame (x:xs) = all (== x) xs
allSame :: Eq a => [a] -> Bool reads as: “for any type a, as long as it supports (==), give me a list of a and I’ll tell you if every element is equal.” This is where Haskell’s expressive type system earns its keep — you write one allSame, and it works correctly and safely on every type that ever gets an Eq instance, checked entirely at compile time.
It’s tempting to think of typeclasses as Haskell’s version of “interfaces” from object-oriented languages, and the analogy is useful — but don’t push it too far. A typeclass instance is chosen by the type of a value, resolved at compile time, not by looking something up on the value itself at runtime the way virtual method dispatch works. There’s no hidden pointer inside a Colour value pointing back to its Eq implementation; the compiler has already decided which (==) to call before the program runs.
Category theorists sometimes describe Haskell’s type system as (approximately) the category Hask: objects are types, and morphisms are functions between them. This is the lens Chapters 7-9 build on — a Functor, Applicative, or Monad isn’t a special language feature bolted onto Haskell; it’s a typeclass, exactly like Eq above, whose operations happen to satisfy laws borrowed directly from category theory.
Type inference: rarely writing what’s already known
Given all this machinery, you might expect Haskell programs to be dense with type annotations. In practice, they’re often sparse — the compiler’s type inference algorithm (Hindley–Milner, refined over decades) can derive the most general type of an expression just from how it’s used:
-- No signature needed; Haskell infers:
-- addPair :: Num a => (a, a) -> a
addPair (x, y) = x + y
Annotations like colourOf :: Fruit -> Colour from Chapter 3 aren’t required by the compiler — they’re written for the reader. In Haskell culture, a top-level type signature is considered close to mandatory documentation: it’s the one comment that’s actually checked for truth every time the code compiles.