Polymorphism (meaning “many forms”) allows code to work with values of different types. Haskell supports two main kinds of polymorphism: parametric polymorphism and ad-hoc polymorphism (via typeclasses).

Parametric Polymorphism

Parametric polymorphism allows functions to operate on values of any type, without knowing or caring what that type is.

Type Variables

Type variables (usually lowercase letters like a, b, c) represent arbitrary types:

id :: a -> a                 -- Identity function works on any type
length :: [a] -> Int         -- Length works on lists of any type
reverse :: [a] -> [a]        -- Reverse works on lists of any type
fst :: (a, b) -> a           -- First works on any pair of types

Examples of Parametrically Polymorphic Functions

-- Identity function
id :: a -> a
id x = x
 
-- Apply a function twice
twice :: (a -> a) -> a -> a
twice f x = f (f x)
 
-- Swap elements in a pair
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)

Parametrically Polymorphic Data Types

Data types can also be parametrically polymorphic:

-- List of elements of type a
data List a = Empty | Cons a (List a)
 
-- Binary tree with values of type a
data Tree a = Leaf | Node a (Tree a) (Tree a)
 
-- Maybe type (optional value)
data Maybe a = Nothing | Just a
 
-- Either type (value of one type or another)
data Either a b = Left a | Right b

Parametricity

Parametricity is a property of parametrically polymorphic functions: the behavior of a function is determined solely by its type signature, not by the types it operates on.

For example, a function with type [a] -> Int can only compute properties like length - it cannot inspect the elements themselves because it knows nothing about type a.

Consequences of Parametricity

  1. Free theorems: We get certain properties “for free” based on the type signature
  2. Limited operations: Functions can only perform operations consistent with all possible types
  3. Predictable behavior: Functions behave uniformly across all types

Example: Functions of Type a -> a

How many functions can have the type a -> a?

With parametricity, there’s only one possible function: the identity function id x = x.

This is because any function of type a -> a must work the same way for all types, and the only thing it can do is return its input unchanged.

Ad-hoc Polymorphism via Typeclasses

Ad-hoc polymorphism allows functions to behave differently depending on the type of their arguments. In Haskell, this is accomplished with typeclasses.

What are Typeclasses?

A typeclass defines a set of functions that must be implemented by any type that wants to be a member of that class.

class Show a where
  show :: a -> String

This says: “For a type a to be a member of the Show class, it must implement the show function.”

Typeclass Instances

Types become members of a typeclass by providing implementations of the required functions:

data Color = Red | Green | Blue
 
instance Show Color where
  show Red = "Red"
  show Green = "Green"
  show Blue = "Blue"

Typeclass Constraints

Functions can require that their type parameters belong to specific typeclasses:

-- A function that works on any type that can be shown
printTwice :: Show a => a -> IO ()
printTwice x = do
  putStrLn (show x)
  putStrLn (show x)

The Show a => part is a typeclass constraint, saying “this works for any type a that is a member of the Show class.”

Common Typeclasses

Eq - Equality Testing

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
 
  -- Default implementations
  x /= y = not (x == y)
  x == y = not (x /= y)

Example instance:

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

Ord - Ordering

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<), (<=), (>), (>=) :: a -> a -> Bool
  max, min :: a -> a -> a
 
  -- Default implementations provided

Ord has Eq as a superclass, meaning any type that is Ord must also be Eq.

Num - Numeric Types

class Num a where
  (+), (-), (*) :: a -> a -> a
  negate, abs, signum :: a -> a
  fromInteger :: Integer -> a

This allows numeric operations on various types (Int, Float, Double, etc.).

Show - String Conversion

class Show a where
  show :: a -> String

Read - Parsing from Strings

class Read a where
  read :: String -> a

deriving Mechanism

For common typeclasses, Haskell can automatically generate instances:

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

This automatically implements:

  • show for Show
  • == and /= for Eq
  • compare, <, etc. for Ord
  • succ, pred, etc. for Enum

Custom Typeclasses

You can define your own typeclasses:

class Drawable a where
  draw :: a -> IO ()
  clear :: a -> IO ()
 
data Shape = Circle Float | Rectangle Float Float
 
instance Drawable Shape where
  draw (Circle r) = putStrLn $ "Drawing circle with radius " ++ show r
  draw (Rectangle w h) = putStrLn $ "Drawing rectangle " ++ show w ++ "x" ++ show h
  
  clear _ = putStrLn "Clearing shape"

Comparing Parametric and Ad-hoc Polymorphism

Parametric PolymorphismAd-hoc Polymorphism (Typeclasses)
One implementation works for all typesDifferent implementations for different types
Functions operate on the structure, not the contentFunctions can inspect and operate on values
”Uniform” behavior across typesType-specific behavior
Examples: id, map, reverseExamples: show, (+), (==)

Key Points to Remember

  1. Parametric polymorphism uses type variables to create functions that work on any type
  2. Parametricity restricts what polymorphic functions can do, leading to predictable behavior
  3. Typeclasses enable ad-hoc polymorphism, allowing different implementations for different types
  4. Common typeclasses include Eq, Ord, Show, Read, and Num
  5. The deriving mechanism automates the creation of typeclass instances