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:
Typeclasses aren’t as old as Haskell itself — they were invented to fix a specific, concrete embarrassment in Standard ML, Haskell’s most direct ancestor. ML could overload arithmetic operators themselves (3 * 3 and 3.14 * 3.14 both worked fine), but a function written in terms of them could not: ML had no way to define one square x = x * x that worked for both Int and Float — you needed squareInt and squareFloat as separate, unrelated functions. Philip Wadler and Stephen Blott’s 1989 paper “How to Make Ad-hoc Polymorphism Less Ad Hoc” introduced type classes specifically to close that gap, generalizing Strachey’s decades-older distinction between “ad-hoc” and “parametric” polymorphism into a real, checkable language feature. The exact square-style problem their paper used to motivate type classes is, not coincidentally, almost identical to the allSame example below.
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.
Eq is one typeclass among a handful used constantly enough to deserve deriving clauses everywhere in this book — Ord and Show, reusing Colour, round out the picture:
data Colour = Red | Yellow | Purple | Green
deriving (Eq, Ord, Show)
ghci> compare Red Yellow
LT
ghci> Red < Green
True
ghci> import Data.List (sort)
ghci> sort [Green, Red, Purple, Yellow]
[Red,Yellow,Purple,Green]
ghci> show Red
"Red"
ghci> print Red
Red
Ord (compare :: a -> a -> Ordering, plus <, <=, >, >= built on top of it) needs some total ordering to work with — for a deriving Ord clause, that ordering is exactly the order the constructors were written in the data declaration, which is why Red < Green above is True and sort recovers Red, Yellow, Purple, Green in declaration order. Show (show :: a -> String) is the other direction from Read (Chapter 3’s read/show pitfall) — turning a value into readable text, which is exactly what print uses internally (print = putStrLn . show) every time a ghci session shows a result without being asked to.
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.
There’s a second difference worth knowing, beyond when the dispatch happens: where an instance is allowed to be declared. A Java class has to say implements Comparable<Point> the moment it’s written — bolting an interface onto an existing, already-compiled class means editing its source or wrapping it in an adapter. A C++ class faces the same constraint with virtual base classes. Haskell has no such requirement: instance Ord Point where ... can be written in a completely different module, months later, by someone who’s never seen Point’s original definition — as long as either the type or the typeclass was defined in your own code somewhere (Haskell calls an instance breaking this rule an orphan instance, and warns about it, but doesn’t forbid it outright). Retroactively giving a type new capabilities without touching its original definition is routine in Haskell in a way it simply isn’t in mainstream object-oriented languages.
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, Ord, and Show above, whose operations happen to satisfy laws borrowed directly from category theory.
The numeric hierarchy: why fromIntegral exists
fromIntegral has already shown up a couple of times in this book, used without much explanation — it’s worth pausing here specifically, now that typeclasses have a name, to see exactly why it’s needed. Haskell’s numeric types aren’t one big interchangeable family; they’re organized into a genuine typeclass hierarchy, and Int and Double sit in different, non-overlapping branches of it:
class Num a where
(+), (-), (*) :: a -> a -> a
fromInteger :: Integer -> a
-- ...
class Num a => Integral a where
div, mod, quot, rem :: a -> a -> a
-- Int and Integer live here
class Num a => Fractional a where
(/) :: a -> a -> a
-- Double and Float live here
Int and Integer are Integral — they support `div` and `mod`, but there’s deliberately no Num instance anywhere that gives them /. Double and Float are Fractional — they support /, but have no `div`. These aren’t two views of the same capability; they’re genuinely different typeclasses, and a type belongs to at most one of them:
ghci> length [1,2,3,4,5] / 2
-- error: no instance for (Fractional Int)
length returns an Int — Integral, not Fractional — so dividing its result with / is a type error, not a runtime surprise. fromIntegral :: (Integral a, Num b) => a -> b is the bridge: it converts a value out of the Integral branch and into any Num type on the other side, Double included:
average :: [Double] -> Double
average xs = sum xs / fromIntegral (length xs)
fromIntegral (length xs) takes length xs :: Int and produces a Double, which / can then actually work with. Every earlier use of fromIntegral in this book — converting a count into something dividable, or Int into Integer — was this exact bridge, crossed the same way each time.
Numeric literals are polymorphic for exactly this reason: 5 isn’t fixed to any one type — its real type is Num a => a, resolved by whatever context it appears in. That’s why 5 :: Int and 5 :: Double are both valid without any conversion function at all; the ambiguity only becomes a problem, and fromIntegral only becomes necessary, once a value has already committed to one specific numeric type (like length’s Int) and needs to cross into another.
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.