Polyglot
Languages Haskell type classes
Haskell § type-classes

Type classes

Type classes are Haskell’s principal polymorphism mechanism. A type class declares a set of operations that any instance type must implement. The mechanism is conceptually similar to interfaces in object-oriented languages and traits in Rust, but with substantial differences: type classes are open (any module may add an instance for any type), they admit ad-hoc polymorphism with full type inference, and they integrate cleanly with parametric polymorphism through constraints. Type classes are one of Haskell’s distinguishing contributions to programming languages and are pervasive throughout the standard library — Eq, Ord, Show, Functor, Monad, dozens more — and throughout idiomatic Haskell code.

This page covers the class declaration mechanism, instance declarations, the standard classes, deriving, and the conventions for using each.

The motivation: ad-hoc polymorphism

Without type classes, polymorphism in Haskell is parametric: a function with a type variable works the same for every type. A function like length :: [a] -> Int does not depend on what a is — it counts elements regardless.

But many operations do depend on the type:

  • Equality: comparing two Int values is different from comparing two String values.
  • Ordering: compare 1 2 and compare "a" "b" use different comparison logic.
  • Display: show 42 and show True produce different strings.

Type classes admit such ad-hoc polymorphism: a class declares the operation, and each type provides its own implementation through an instance.

Class declarations

A class declaration introduces a set of operations:

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

    -- default implementations
    x == y = not (x /= y)
    x /= y = not (x == y)

The class Eq a where introduces a class named Eq with one type parameter a. The class declares two operations, == and /=, with their type signatures. Default implementations admit a class to provide some methods automatically when others are defined.

The simpler form (just operations, no defaults):

class Show a where
    show :: a -> String

A type variable in the operation’s signature must refer to the class’s parameter — the operations are parameterised by the class’s type.

Instance declarations

An instance declares that a type belongs to a class:

data Color = Red | Green | Blue

instance Show Color where
    show Red   = "red"
    show Green = "green"
    show Blue  = "blue"

instance Eq Color where
    Red   == Red   = True
    Green == Green = True
    Blue  == Blue  = True
    _     == _     = False

Each instance provides definitions for the class’s operations. The compiler uses the instance to dispatch calls of the class’s operations on values of the type:

show Red                -- "red"
Red == Green            -- False
print Red               -- print uses Show internally

Instances may be declared in any module — the module that defines the type, the module that defines the class, or any third module. The mechanism is one of the language’s distinguishing features: classes are open in the sense that the set of instances grows over time without modifying the original declaration.

Orphan instances

An instance is an orphan if neither the type nor the class is defined in the same module:

-- module A: defines Color
-- module B: defines Show
-- module C: declares `instance Show Color where ...`

-- the instance in C is an orphan

The compiler warns about orphan instances. The trade-off:

  • Pros: orphan instances admit retro-fitting type-class membership for types and classes from other libraries.
  • Cons: two libraries may both define an orphan instance for the same Show Color, producing a conflict.

The conventional discipline is to avoid orphans where possible: define the instance in the module that defines either the type or the class.

Constraints

A function may constrain its type variables to belong to type classes:

sort :: Ord a => [a] -> [a]
elem :: Eq a => a -> [a] -> Bool
show :: Show a => a -> String

The Ord a => is a context: it asserts that a must be an instance of Ord for the function to be called. The context becomes part of the function’s signature; the compiler verifies it at every call site.

Multiple constraints are written as a tuple:

process :: (Eq a, Show a) => a -> a -> String
process x y
    | x == y    = "equal: " ++ show x
    | otherwise = "different: " ++ show x ++ " and " ++ show y

The function above requires both Eq and Show instances for a.

Default methods

A class may provide default implementations:

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

    x == y = not (x /= y)        -- default in terms of /=
    x /= y = not (x == y)        -- default in terms of ==

An instance may override either or both:

instance Eq Color where
    Red   == Red   = True
    Green == Green = True
    Blue  == Blue  = True
    _     == _     = False
    -- /= uses the default

The mechanism reduces boilerplate: an instance need only provide enough methods to determine the rest. The conventional discipline is to define the minimal complete definition — the smallest set that lets the defaults compute the rest.

A type-class declaration’s documentation typically lists the minimal complete definition explicitly:

-- |
-- Minimal complete definition: 'compare' or '<='
class (Eq a) => Ord a where
    compare :: a -> a -> Ordering
    (<), (<=), (>), (>=) :: a -> a -> Bool
    -- ...

Superclasses

A class may require its instances to also be instances of another class:

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

The (Eq a) => in the class header declares that Eq a is a superclass of Ord a — every type that is an instance of Ord must also be an instance of Eq. The mechanism admits expressing relationships between classes; the compiler verifies the constraint when an instance is declared.

The class hierarchy of the standard library:

Eq
├── Ord
└── ...

Functor
└── Applicative
    └── Monad
    └── Alternative
        └── MonadPlus

Foldable
└── Traversable

Each arrow is a superclass constraint. The hierarchy is occasionally awkward — Functor was made a superclass of Applicative only in 2014, and Applicative was made a superclass of Monad only in 2015 (the Functor-Applicative-Monad Proposal). Earlier code did not enforce the constraint.

The dictionary-passing implementation

Haskell type classes are implemented through dictionary passing. The compiler converts a class to a record of methods (the dictionary) and converts every constrained function to one that takes the dictionary as an extra argument:

-- Source:
class Eq a where (==) :: a -> a -> Bool
elem :: Eq a => a -> [a] -> Bool

-- Conceptual desugaring:
data EqDict a = EqDict { eq :: a -> a -> Bool }
elem' :: EqDict a -> a -> [a] -> Bool
elem' dict x = any (\y -> eq dict x y)

At each call site, the compiler determines the type and supplies the appropriate dictionary. The mechanism is invisible at the source level but explains some subtleties:

  • Type-class operations have a small overhead (the dictionary lookup).
  • The compiler’s INLINE and specialisation pragmas can eliminate the dictionary at the call site, producing code as efficient as a non-polymorphic version.
  • The mechanism is the source of the “dictionary” terminology in advanced Haskell discussions.

Standard classes

The Haskell standard library defines a substantial set of type classes:

Equality and ordering

ClassOperationsUse
Eq==, /=Equality test
Ordcompare, <, <=, >, >=, min, maxTotal ordering (requires Eq)

Display and parse

ClassOperationsUse
ShowshowConvert a value to a String
Readread, readsParse a value from a String

Show is conventionally derived; Read is rarely useful in production (parsing is typically done with parsec-style libraries instead).

Numeric

ClassOperationsUse
Num+, -, *, negate, abs, signum, fromIntegerNumeric types
Fractional/, recip, fromRationalFractional types (extends Num)
Floatingpi, exp, log, sqrt, sin, etc.Floating-point types (extends Fractional)
Integraldiv, mod, quot, rem, toIntegerInteger types (extends Real, Enum)
RealtoRationalRational-convertible types (extends Num, Ord)
RealFractruncate, round, floor, ceilingRational fractional types
RealFloatfloatDigits, floatRange, etc.Real floating types

The numeric class hierarchy is large and historically considered too granular; modern code typically uses a few of the classes (Num, Fractional) directly.

Enumerable and bounded

ClassOperationsUse
Enumsucc, pred, toEnum, fromEnum, enumFromTo, …Sequential types (admits [1..10] syntax)
BoundedminBound, maxBoundTypes with a min and max value

Monadic

ClassOperationsUse
Functorfmap, <$>Type constructors of kind * -> * admitting structure-preserving map
Applicativepure, <*>, liftA2Functors with embedding and apply
Monadreturn, >>=, >>Applicatives with sequential composition
MonadFailfailMonads with pattern-match failure

The monadic hierarchy is the central machinery of effectful Haskell; treated in Functors and applicatives and Monads.

Foldable and traversable

ClassOperationsUse
Foldablefoldr, foldl, foldMap, length, null, elem, …Containers admitting reduction
Traversabletraverse, sequenceA, mapM, sequenceContainers admitting effectful traversal

IsString and Num literals

ClassOperationsUse
IsStringfromStringTypes admitting string-literal conversion (with OverloadedStrings)
NumfromIntegerTypes admitting numeric-literal conversion
FractionalfromRationalTypes admitting fractional-literal conversion

The literal-related classes admit polymorphic literals: 42 has type Num a => a, the type of any Num instance.

Deriving

A type may derive instances of certain classes automatically:

data Color = Red | Green | Blue
    deriving (Eq, Ord, Show, Read, Enum, Bounded)

The compiler synthesises the instances based on the type’s structure. The conventionally-derivable classes are:

  • Eq, Ord — structural equality and ordering.
  • Show, Read — generated string representation and parsing.
  • Enum, Bounded — for finite enumerations.
  • Functor, Foldable, Traversable — for parametric types (with DeriveFunctor, DeriveFoldable, DeriveTraversable extensions).
  • Generic — for Template Haskell-style introspection (with DeriveGeneric).

The derived instances follow conventional structural definitions:

-- For data Color = Red | Green | Blue, the derived Show instance produces:
show Red   = "Red"
show Green = "Green"
show Blue  = "Blue"

-- The derived Eq instance:
Red   == Red   = True
Green == Green = True
Blue  == Blue  = True
_     == _     = False

For records, derived Show produces the record syntax:

data Person = Person { name :: String, age :: Int } deriving Show

show (Person "Alice" 30)
-- "Person {name = \"Alice\", age = 30}"

Deriving strategies

GHC admits several deriving strategies with the DerivingStrategies extension:

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingVia #-}

newtype Age = Age Int
    deriving stock (Eq, Ord, Show)        -- standard derivation
    deriving newtype (Num)                  -- inherit from the underlying Int
    deriving via (Sum Int) Monoid            -- derive Monoid through Sum

The strategies:

  • stock — the conventional auto-derivation.
  • newtype — inherit the instance from the underlying type (only for newtypes).
  • via — derive through an intermediate type (the DerivingVia extension).
  • anyclass — derive a generic-default empty instance.

DerivingVia is one of GHC’s most powerful extensions; it admits substantial code reuse for user-defined type classes.

Multi-parameter type classes

A class may have more than one parameter (with the MultiParamTypeClasses extension):

{-# LANGUAGE MultiParamTypeClasses #-}

class Convert a b where
    convert :: a -> b

instance Convert Int Double where
    convert = fromIntegral

instance Convert String Text where
    convert = T.pack

The mechanism is occasionally useful but less common than single-parameter classes; it admits relations between types but produces ambiguity in inference (the compiler may not know which instance to pick).

The FunctionalDependencies extension admits constraints between parameters:

{-# LANGUAGE FunctionalDependencies #-}

class Convert a b | a -> b where
    convert :: a -> b

The a -> b declares that a determines b — at most one instance per a. The mechanism resolves ambiguity in inference.

A modern alternative is associated types via the TypeFamilies extension:

{-# LANGUAGE TypeFamilies #-}

class Convert a where
    type Result a
    convert :: a -> Result a

instance Convert Int where
    type Result Int = Double
    convert = fromIntegral

The two mechanisms are largely equivalent; the choice is stylistic.

Common patterns

Constraint-driven generic programming

sumWithDefault :: (Num a, Foldable t) => a -> t a -> a
sumWithDefault d xs
    | null xs   = d
    | otherwise = sum xs

The function works for any Foldable of any Num.

Newtype-based instance overriding

newtype Min a = Min a
instance (Ord a) => Semigroup (Min a) where
    Min x <> Min y = Min (min x y)

newtype Max a = Max a
instance (Ord a) => Semigroup (Max a) where
    Max x <> Max y = Max (max x y)

The pattern admits multiple Semigroup instances for the same underlying type by wrapping in newtypes.

Type-class-driven serialisation

class ToJSON a where
    toJSON :: a -> Value

instance ToJSON Int where
    toJSON = Number . fromIntegral

instance ToJSON String where
    toJSON = String . T.pack

instance (ToJSON a) => ToJSON [a] where
    toJSON = Array . V.fromList . map toJSON

The pattern is the conventional Haskell idiom for serialisation libraries (Aeson uses essentially this structure).

A note on type classes vs interfaces

Type classes differ from interfaces in object-oriented languages:

AspectType classesInterfaces
Where instances are declaredAnywhereAt the type’s declaration site
Multiple methodsYesYes
Default methodsYesYes (recently in some languages)
SubtypingNoYes
DispatchAt compile time (typically)At runtime
Open vs closedOpen (instances added retroactively)Closed (interfaces fixed at type definition)
Constraints on type variablesYes(Through generics)

The principal practical consequence: type classes admit retroactive instance declarations, which is the conventional Haskell idiom for adding behaviour to types from other libraries. Interfaces in Java/C# require modifying the type’s declaration; type classes do not.

The trade-off: type classes do not admit subtype polymorphism (no implicit “use a more-specific instance”); inheritance hierarchies are not directly representable. The Haskell idiom for hierarchies is multi-class constraints ((Eq a, Ord a) =>) rather than single-inheritance dispatch.