100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

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.

Type SystemIntermediate10 min readJul 10, 2026
Analogies

Type Classes as Shared Interfaces

A type class declares a set of function signatures that any conforming type must implement, functioning as an interface contract rather than a container of data. class Eq a where (==) :: a -> a -> Bool; (/=) :: a -> a -> Bool says: any type a that wants to be an instance of Eq must provide an == (and /=) function comparing two values of that type. This is Haskell's mechanism for ad-hoc polymorphism -- the same function name == behaves differently for Int, Bool, or a custom Color type, with the compiler picking the right implementation based on the concrete type involved, resolved via an implicit 'dictionary' of functions passed behind the scenes.

🏏

Cricket analogy: Just as the ICC requires every international team to field exactly eleven players and follow the same over-limit rules regardless of whether it's Australia or Afghanistan, a type class like Eq requires every instance type to implement ==, with the actual comparison logic differing per type just as playing styles differ per team.

Declaring Instances and Deriving

You make a type conform to a class by writing an instance declaration, instance Eq Color where Red == Red = True; Blue == Blue = True; _ == _ = False, providing concrete implementations for every method the class requires. For classes like Eq, Ord, Show, and Read whose behavior follows a mechanical, structural pattern, GHC can generate the instance automatically via deriving (Eq, Show, Ord) appended to a data declaration, saving you from writing boilerplate comparison and printing code by hand for every constructor.

🏏

Cricket analogy: Writing a custom instance Eq Color where ... is like a franchise league writing its own bespoke tie-breaker rules instead of using the ICC default, while deriving (Eq) is like adopting the standard net-run-rate tie-breaker automatically without drafting new regulations.

haskell
data Color = Red | Green | Blue
  deriving (Eq, Show, Ord)   -- auto-generated ==, show, compare

class Describable a where
  describe :: a -> String
  describe _ = "no description available"   -- default method

instance Describable Color where
  describe Red   = "warm and bold"
  describe Green = "calm and natural"
  describe Blue  = "cool and deep"

-- A constrained polymorphic function: works for ANY Ord instance
biggest :: Ord a => [a] -> a
biggest = maximum

main :: IO ()
main = do
  print (Red == Red)        -- True   (from deriving Eq)
  print (Red < Blue)        -- True   (from deriving Ord, ctor order)
  putStrLn (describe Green) -- "calm and natural"
  print (biggest [3, 1, 4, 1, 5 :: Int])   -- 5

Class Constraints on Polymorphic Functions

A function signature like biggest :: Ord a => [a] -> a carries a class constraint before the =>, meaning biggest works for a list of any type a, as long as that type has an Ord instance -- it is polymorphic, but not for every possible type, only for types that support ordering. Under the hood, GHC implements this by silently passing a 'dictionary' of the required functions (like compare and <=) alongside the polymorphic argument, which is why calling biggest [Red, Blue] only compiles if Color derives or defines Ord; leaving that deriving clause off produces a clear compile-time error rather than a runtime surprise.

🏏

Cricket analogy: A tournament eligibility rule like 'any bowler with a first-class average under 25 may be selected' constrains selection to a subset of players who satisfy that condition, mirroring how Ord a => [a] -> a constrains biggest to only types a that satisfy the Ord capability.

Commonly Used Standard Type Classes

A handful of type classes recur throughout everyday Haskell: Eq for equality (==, /=), Ord for ordering (compare, <, >=, and maximum/minimum/sort built on top of it), Show for converting a value to a human-readable String (used implicitly by GHCi and print), Num for arithmetic operations like + and *, and Functor for mapping a function over a wrapped value via fmap (which works uniformly across Maybe, lists, Either, and many other types). Recognizing these five by sight is enough to read the vast majority of type signatures in real Haskell code, since almost every constraint you encounter -- Ord a =>, Show a =>, Num a => -- is drawn from this small, well-known set.

🏏

Cricket analogy: A commentary team relies on a small set of recurring stat categories -- strike rate, economy rate, average -- to describe almost any player, the same way most Haskell signatures lean on a small recurring set of classes like Eq, Ord, Show, and Num.

Class methods can carry default implementations, like (/=) x y = not (x == y) inside the Eq class declaration itself, so an instance only needs to define == and gets a correct /= for free -- overriding the default is only necessary when a more efficient direct implementation is possible.

deriving (Ord) bases its comparison purely on the order constructors are listed in the data declaration -- data Priority = Low | Medium | High deriving (Ord) makes Low < High true because Low is listed first, not because of any inherent numeric meaning, so reordering the constructors silently changes the sort order everywhere the derived Ord instance is used.

  • A type class declares a set of function signatures (like == for Eq) that any conforming type must implement via an instance declaration.
  • Type classes enable ad-hoc polymorphism: the same function name behaves differently per type, resolved through an implicit dictionary passed by the compiler.
  • deriving (Eq, Show, Ord) on a data declaration auto-generates mechanical instances instead of requiring hand-written boilerplate.
  • A constraint like Ord a => before a signature's => restricts a polymorphic function to only types that implement that class.
  • Eq, Ord, Show, Num, and Functor are the five type classes that appear in the overwhelming majority of everyday Haskell signatures.
  • Class declarations can supply default method implementations, so an instance only needs to define the minimal required methods.
  • Derived Ord instances depend on constructor declaration order, which can silently produce an unintended sort order if not considered carefully.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#HaskellStudyNotes#TypeClassesExplained#Type#Classes#Explained#Shared#OOP#StudyNotes#SkillVeris