Skip to content
Home
Type Classes: Ad-Hoc Polymorphism in FP

Type Classes: Ad-Hoc Polymorphism in FP

Functional Programming Functional Programming 8 min read 1677 words Beginner ExcellentWiki Editorial Team

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
    _     == _     = False

Type 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 = show

Common Type Classes

Eq and Ord

class Eq a where
    (==), (/=) :: a -> a -> Bool

class Eq a => Ord a where
    compare :: a -> a -> Ordering
    (<), (<=), (>=), (>) :: a -> a -> Bool

Ord 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 length

Functor 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  -- Nothing

Monad

class Applicative m => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a
    return = pure

Monad 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 associativity

These 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 Concepts and Further Reading

Understanding type classes requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.

The relationship between type classes and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.

For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of type classes. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.

Section: Functional Programming 1677 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top