There are two very different ways a function can work at many types.
Parametric polymorphism — the length counts a list's elements without ever looking
at what they are. Ad-hoc polymorphism — overloading — runs
different code depending on the type:
Type classes (Wadler & Blott, 1989) made ad-hoc polymorphism principled.
A class is a named interface of overloaded operations; an instance supplies that interface for a
particular type; and a function may carry constraints —
"
A class declaration names an overloaded interface. A class constraint
The type of member is a qualified type: the part before
Note the last instance is itself conditional:
Here is how the compiler makes it all real, with no run-time type inspection whatsoever. Each
class becomes a record type — a dictionary holding its methods. Each
instance becomes a dictionary value. Each constraint
So the qualified type elaborates by turning every
The constraint became a plain parameter. Everything a type class does is captured by this one move —
which is exactly why type classes are "just" a disciplined, type-directed, automatically-inserted form
of passing an interface record. The programmer writes eq x y; the compiler rewrites it to
dict.eq(x, y), having threaded dict in for them.
Below is the dictionary translation done explicitly in TypeScript: dictionaries are records, instances
are values, the conditional
eqArray(eqNumber) is instance resolution made concrete: to compare two
number[]s the compiler needed
Because dispatch is by type, not by a runtime value, type classes can do something ordinary
object-oriented method dispatch cannot: choose the instance from the result type.
Haskell's read :: Read a => String -> a parses into whatever type the context
demands; mempty :: Monoid a => a and fromInteger :: Num a => Integer -> a
conjure a value of a type that appears nowhere in the arguments. In dictionary terms this is trivial —
the dictionary for the result type is simply threaded in — but it is impossible for a
this-receiver dispatch that needs an object in hand to pick the method.
Ask an object-oriented programmer to dispatch parse("42") to an Int parser or a
Bool parser and they are stuck: there is no receiver object yet to look a method up on — the
value is what we are trying to create. Type classes shrug. Because the instance is selected by
the static type and delivered as a dictionary argument, the compiler reads the expected result
type from context — let n :: Int = read "42" — and passes the Read Int
dictionary. This "return-type polymorphism" underlies fromInteger (so the literal
5 can be an Int, a Double, or a Complex depending on
use), minBound, mempty, and the whole numeric tower. It is the clearest single
demonstration that type-class dispatch is fundamentally different from — and strictly more expressive
than — the vtable dispatch of objects.
A type class carries a hidden, load-bearing promise: for any given class and type there is
at most one instance in the entire program — global uniqueness, or
coherence. It matters because a data structure can bake a dictionary into itself: a
Set ordered by one Ord Int instance would silently corrupt if another part of
the program compared its elements by a different one. That is why Haskell forbids two
instance Eq Ints, and why orphan instances (an instance defined in neither
the class's module nor the type's) are discouraged — they let two libraries each define an instance for a
third party's type, and now which one you get depends on imports, breaking coherence. Contrast Scala's
implicit/given parameters, which are just dictionary passing but
without the global-uniqueness rule: more flexible (locally scoped instances) at the cost that
two modules can disagree on "the" Ordering for a type. Coherence is the price of, and the
guarantee behind, the type class's magic.