Type Classes: Ad-Hoc Polymorphism in FP
Type classes are a language feature that enables ad-hoc polymorphism — functions that work on multiple types with type-specific implementations. Originated in Haskell, type classes have influenced traits in Rust, implicits in Scala, protocols in Clojure, and concepts in C++20.
What Are Type Classes?
A type class defines a set of operations that a type must support. Types declare which type classes they implement and provide the specific implementations.
-- Define a type class
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x /= y = not (x == y) -- Default implementation
-- Implement the type class for a specific type
instance Eq Integer where
x == y = integerEq x y -- Built-in equality
-- Implement for a custom type
data Color = Red | Green | Blue
instance Eq Color where
Red == Red = True
Green == Green = True
Blue == Blue = True
_ == _ = FalseType Classes vs OOP Interfaces
Type classes look similar to interfaces, but differ in fundamental ways:
Open World Assumption
In OOP, a class declares which interfaces it implements. You cannot retroactively add an interface to an existing class. With type classes, you can define new instances for existing types:
-- Retroactively add JSON serialization to a standard type
instance ToJSON Color where
toJSON Red = String "red"
toJSON Green = String "green"
toJSON Blue = String "blue"Static Dispatch vs Dynamic Dispatch
Type classes typically dispatch at compile time (static dispatch), avoiding the runtime overhead of virtual method tables. The compiler generates specialized code for each type.
Multiple Parameters
Type classes can depend on multiple types:
class Convertible a b where
convert :: a -> b
instance Convertible String Integer where
convert = read
instance Convertible Integer String where
convert = showCommon Type Classes
Eq and Ord
class Eq a where
(==), (/=) :: a -> a -> Bool
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> BoolOrd requires Eq — this is a type class constraint. Any type implementing Ord must also implement Eq.
Functor
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- fmap over a list
fmap (+1) [1, 2, 3] -- [2, 3, 4]
-- fmap over Maybe
fmap (+1) (Just 5) -- Just 6
fmap (+1) Nothing -- Nothing
-- fmap over IO
fmap length getLine -- Read a line, return its lengthFunctor represents types that can be mapped over. It is the foundation of many functional abstractions.
Applicative
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
-- Combine independent effects
(+) <$> Just 3 <*> Just 5 -- Just 8
(+) <$> Just 3 <*> Nothing -- NothingMonad
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return = pureMonad represents sequential computation with effects. It is perhaps the most famous type class in functional programming.
Foldable and Traversable
class Foldable t where
foldr :: (a -> b -> b) -> b -> t a -> b
foldl :: (b -> a -> b) -> b -> t a -> b
null :: t a -> Bool
length :: t a -> Int
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)Foldable abstracts over data structures that can be folded. Traversable extends this to effectful traversals — applying an effectful function to each element and collecting the results.
Type Classes in Other Languages
Rust Traits
trait Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
}
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Red => write!(f, "red"),
Color::Green => write!(f, "green"),
Color::Blue => write!(f, "blue"),
}
}
}
fn print<T: Display>(value: T) {
println!("{}", value);
}Scala Implicits
trait Show[A] {
def show(a: A): String
}
object Show {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val stringShow: Show[String] = (a: String) => a
def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42) // "42"
Show("hello") // "hello"Swift Protocols
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
struct Color: Equatable {
let name: String
}
extension Color: Equatable {
static func == (lhs: Color, rhs: Color) -> Bool {
return lhs.name == rhs.name
}
}Laws and Reasoning
Type classes come with laws that implementations should satisfy:
fmap id = id -- Functor identity
fmap (f . g) = fmap f . fmap g -- Functor composition
return x >>= f = f x -- Monad left identity
m >>= return = m -- Monad right identity
(m >>= f) >>= g = m >>= (\x -> f x >>= g) -- Monad associativityThese laws enable equational reasoning — you can transform code based on algebraic properties without knowing the concrete types or implementations.
FAQ
What is the difference between a type class and an interface in Java?
Interfaces use dynamic dispatch (virtual method table lookup at runtime). Type classes use static dispatch (monomorphization at compile time). Interfaces are closed (a class must declare which interfaces it implements). Type classes are open (you can define new instances for existing types without modifying them).
Can I simulate type classes in JavaScript?
Yes, using “dictionary passing” — passing an object of methods as an argument. Libraries like fp-ts and sanctuary use this pattern extensively. It’s less ergonomic than native type classes but achieves the same goals.
What is the Constraint kind in Haskell?
Type class constraints are written with => (double arrow). sort :: Ord a => [a] -> [a] means “sort works for any type a that implements Ord.” Multiple constraints are separated by commas: (Eq a, Show a) => a -> String.
Why do type classes have laws?
Laws ensure that generic code behaves predictably. If a Functor instance violates fmap id = id, then code that relies on this property will be incorrect. Laws are contracts between the implementer and the consumer of a type class instance.
What are Deriving and DerivingVia?
GHC can automatically generate instances for common type classes using deriving:
data Color = Red | Green | Blue
deriving (Eq, Ord, Show, Read)DerivingVia extends this to derive instances via an existing instance of another type, reducing boilerplate for wrapper types.
Related: Learn category theory and monads explained.
Type Classes in Detail
Type classes define interfaces that types can implement, enabling ad-hoc polymorphism without inheritance. Haskell’s type classes include Eq, Ord, Show, Read, Semigroup, Monoid, Functor, Applicative, Monad, Foldable, Traversable. Instance declarations provide implementations. Multi-parameter type classes allow relationships between types. Deriving mechanisms automatically generate instances. Type class laws formalize expected behavior: x <> mempty = x (right identity). Scala 3’s given/using replaces implicits with clearer syntax. Rust’s traits are equivalent. Swift’s protocols and Go’s interfaces serve similar purposes. Type classes enable generic programming without runtime overhead — resolution happens at compile time.
Type Classes vs OOP Interfaces
Type classes are open (any type can implement them) while OOP interfaces require declaration at definition. Type classes dispatch on the type, not the object, enabling retroactive extension. Both achieve polymorphism, but type classes work better for algebraic data types and generic algorithms without subtype hierarchy.
Advanced Type Class Features
Multi-parameter type classes allow relationships between multiple types, such as a conversion class Convertible a b. Functional dependencies (| a -> b) resolve ambiguous instances by declaring that one type uniquely determines another. Associated types (type family) in Haskell enable type-level functions within type classes. Default signatures provide fallback implementations that instances can override. Deriving strategies (DeriveAnyClass, GeneralizedNewtypeDeriving) automate boilerplate. Quantified constraints (forall a. Show a => Show (f a)) express complex dependencies. The ConstraintKinds extension treats type class constraints as first-class types, enabling constraint-passing patterns. Orphan instances (instances defined in a module that doesn’t define the type or the class) should be avoided because they can cause coherence issues — GHC warns about them by default. Backpack, Haskell’s module system, supports signature-based type class abstraction for large-scale modular development. In practice, most Haskell projects use a handful of common type classes (Eq, Ord, Show, Functor, Applicative, Monad) with deriving mechanisms handling the boilerplate automatically. Understanding type classes deepens your grasp of how polymorphic code is type-checked and compiled.
Related Articles
Functional Programming
Haskell: A Beginner's Guide to Pure Functional Programming
Learn Haskell — pure functions, lazy evaluation, type classes, monads, GHC tooling, and building reliable concurrent applications.
Rust
Rust Traits and Generics: Writing Reusable Code
Learn Rust traits and generics — defining traits, trait bounds, generic functions, associated types, and writing polymorphic Rust code.
C/C++
C++ Object-Oriented Programming: Classes, Inheritance, and Polymorphism
Learn C++ OOP — classes, constructors, inheritance, virtual functions, polymorphism, RAII, and modern C++ best practices.
Functional Programming
Category Theory for Programmers: A Gentle Introduction
Learn category theory basics — categories, functors, natural transformations, and how they apply to functional programming.
Functional Programming
Currying and Function Composition in Depth
Master currying and function composition in functional programming. Partial application, pipeline patterns, and point-free style with practical examples.