Day 3 of 5
⏱ ~60 minutes
Haskell in 5 Days — Day 3

Type Classes & Custom Types

Haskell's type system lets you define rich domain models and attach behaviors via type classes. Today you create custom data types, make them instances of standard type classes, and see how the type system enforces program correctness.

Algebraic Data Types

ADTs model domain concepts precisely. 'data Shape = Circle Double | Rectangle Double Double | Triangle Double Double Double' is a sum type — a Shape is one of three variants. 'data Person = Person { name :: String, age :: Int }' is a product type with named fields (record syntax). Pattern match on constructors to handle each case. The compiler warns if you miss a case.

Deriving Type Classes

Add 'deriving (Eq, Ord, Show, Read, Enum, Bounded)' to automatically generate standard instances. Eq: (==) and (/=). Ord: (<), compare. Show: show turns a value to a String. Read: read parses a String. Deriving handles nested types automatically. For custom behavior, write a manual instance: 'instance Show Shape where show (Circle r) = ...'

Type Classes in Depth

A type class defines an interface. 'class Container f where empty :: f a; insert :: a -> f a -> f a; toList :: f a -> [a]'. Types implement it: 'instance Container [] where ...'. Functor, Foldable, and Traversable are the standard type classes for container types. A type that is a Functor can use fmap; a Foldable can use foldr/sum/length; a Traversable can sequence effects through a structure.

haskell
data Shape
  = Circle    Double
  | Rectangle Double Double
  | Triangle  Double Double Double
  deriving (Show, Eq)

area :: Shape -> Double
area (Circle r)        = pi * r * r
area (Rectangle w h)   = w * h
area (Triangle a b c)  =
  let s = (a + b + c) / 2
  in  sqrt (s * (s-a) * (s-b) * (s-c))  -- Heron's formula

perimeter :: Shape -> Double
perimeter (Circle r)       = 2 * pi * r
perimeter (Rectangle w h)  = 2 * (w + h)
perimeter (Triangle a b c) = a + b + c

-- Type class instance for ordering by area
instance Ord Shape where
  compare s1 s2 = compare (area s1) (area s2)

-- Usage
shapes :: [Shape]
shapes = [Circle 5, Rectangle 3 4, Triangle 3 4 5]

largest :: Shape
largest = maximum shapes  -- uses Ord instance
💡
Use record syntax for data types with more than 2 fields. It generates named accessor functions automatically and makes field updates readable: 'person { age = 30 }' instead of constructing a new value manually.
📝 Day 3 Exercise
Model a Card Game
  1. Define data types for Suit (Clubs/Diamonds/Hearts/Spades) and Rank (Two..Ace)
  2. Define a Card type as a product of Suit and Rank
  3. Derive Show and Eq for all three types
  4. Write a function that generates a full 52-card deck
  5. Write a function that computes the Blackjack value of a hand (handle Ace as 1 or 11)

Day 3 Summary

  • Sum types (data T = A | B | C) model alternatives; product types model combinations
  • Deriving generates Eq, Ord, Show, Read implementations automatically
  • Pattern matching on ADT constructors is exhaustive — the compiler warns on missing cases
  • Type classes define interfaces; instances provide implementations
  • Record syntax generates accessor functions and enables field-update syntax
Challenge

Design an ADT for a simple expression language: numbers, variables, addition, multiplication, and let-bindings. Write an eval :: Expr -> Map String Int -> Int evaluator and a pretty :: Expr -> String pretty-printer.

Finished this lesson?