Type Classes
What Are Type Classes?
Section titled “What Are Type Classes?”Type classes are Haskell’s mechanism for ad hoc polymorphism — defining interfaces that types can implement. Unlike OOP interfaces, type classes are separate from data types and can be added after the type is defined (unlike in most OOP languages). A type class declares a set of functions (called methods) that implementing types must define.
-- Define a type classclass Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool
-- Make a type an instance of the classinstance Eq Bool where True == True = True False == False = True _ == _ = False
x /= y = not (x == y)The deriving Mechanism
Section titled “The deriving Mechanism”Haskell can automatically generate instances for many standard type classes:
data Color = Red | Green | Blue deriving (Show, Eq, Ord, Enum, Bounded)
-- This automatically generates:-- show Red = "Red", show Green = "Green", show Blue = "Blue"-- Red == Red = True, Red == Green = False, etc.-- Red < Green = True, Green < Blue = True, etc.-- [Red .. Blue] = [Red, Green, Blue]-- minBound = Red, maxBound = BlueThe deriving clause works by generating boilerplate code at compile time. It is available for these standard type classes: Eq, Ord, Enum, Bounded, Show, Read.
Show and Read
Section titled “Show and Read”Show converts values to their string representation:
class Show a where show :: a -> String showList :: [a] -> ShowS -- optional
-- Derived instancesdata Point = Point Double Double deriving (Show)
-- Point 1.0 2.0 => "Point 1.0 2.0"
-- Custom instanceinstance Show Point where show (Point x y) = "(" ++ show x ++ ", " ++ show y ++ ")" -- Point 1.0 2.0 => "(1.0, 2.0)"Read is the inverse of Show — it parses a string into a value:
class Read a where readsPrec :: Int -> ReadS a -- ReadS a = String -> [(a, String)]
-- read parses a string into any Read typeread "42" :: Int -- => 42read "True" :: Bool -- => Trueread "[1,2,3]" :: [Int] -- => [1, 2, 3]read "(1,'a')" :: (Int, Char) -- => (1, 'a')
-- read is partial: provide type annotation or use readMaybereadMaybe "42" :: Maybe Int -- => Just 42readMaybe "abc" :: Maybe Int -- => Nothingclass Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool -- Default: x /= y = not (x == y)
-- Derived for algebraic data typesdata Shape = Circle Double | Rectangle Double Double deriving (Eq)-- Circle 1.0 == Circle 1.0 = True-- Circle 1.0 == Rectangle 1.0 1.0 = False
-- Custom instance for types with special equalitydata MyFloat = MyFloat Doubleinstance Eq MyFloat where MyFloat a == MyFloat b = abs (a - b) < 1e-9class Eq a => Ord a where compare :: a -> a -> Ordering -- Ordering = LT | EQ | GT (<), (<=), (>), (>=) :: a -> a -> Bool min, max :: a -> a -> a
-- Derived instances order constructors by definition orderdata Priority = Low | Medium | High deriving (Eq, Ord)-- Low < Medium < High (order of constructor declaration)
-- compare returns LT, EQ, or GTcompare 1 5 -- => LTcompare 5 5 -- => EQcompare 5 1 -- => GTEnum and Bounded
Section titled “Enum and Bounded”class Enum a where succ, pred :: a -> a toEnum :: Int -> a fromEnum :: a -> Int enumFromTo :: a -> a -> [a] enumFromThenTo :: a -> a -> a -> [a]
-- Succ and pred move through enumerationsucc True -- => error (no successor)succ Red -- => Green (if Red < Green < Blue)pred 'b' -- => 'a'
-- Ranges[1..10] -- => [1,2,3,4,5,6,7,8,9,10]['a'..'z'] -- => "abcdefghijklmnopqrstuvwxyz"[Red..Blue] -- => [Red, Green, Blue]Bounded
Section titled “Bounded”class Bounded a where minBound :: a maxBound :: a
minBound :: Int -- => -9223372036854775808 (on 64-bit)maxBound :: Int -- => 9223372036854775807maxBound :: Char -- => '\1114111'minBound :: Bool -- => FalsemaxBound :: Bool -- => TrueNum and Numeric Type Classes
Section titled “Num and Numeric Type Classes”class Eq a => Num a where (+), (-), (*) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a fromInteger :: Integer -> a
class Num a => Fractional a where (/) :: a -> a -> a fromRational :: Rational -> a
class (Real a, Fractional a) => RealFrac a where properFraction :: (Integral b) => a -> (b, a) truncate, round, ceiling, floor :: (Integral b) => a -> bNumeric literals are polymorphic:
-- 42 has type (Num a) => a-- It is actually fromInteger 4242 :: Int -- => 4242 :: Double -- => 42.042 :: Integer -- => 42
-- 3.14 has type (Fractional a) => a-- It is actually fromRational (314 % 100)3.14 :: Float -- => 3.143.14 :: Double -- => 3.14Functor
Section titled “Functor”The Functor type class represents types that can be “mapped over”:
class Functor f where fmap :: (a -> b) -> f a -> f b -- Also: (<$) :: a -> f b -> f a -- replace mapped value with constant
-- Laws:-- 1. Identity: fmap id = id-- 2. Composition: fmap (f . g) = fmap f . fmap g-- Maybe is a Functorfmap (*2) (Just 5) -- => Just 10fmap (*2) Nothing -- => Nothing-- Using fmap infix as <$>-- import Control.Applicative ((<$>))(*2) <$> (Just 5) -- => Just 10
-- List is a Functorfmap (*2) [1, 2, 3] -- => [2, 4, 6]
-- IO is a Functorfmap length getLine -- reads a line and returns its length-- fmap applies the function to the result of the IO action
-- ((->) r) is a Functor (function type)-- fmap :: (a -> b) -> (r -> a) -> (r -> b)-- This is just function composition!instance Functor ((->) r) where fmap = (.)Making a Type a Functor
Section titled “Making a Type a Functor”data Tree a = Leaf a | Branch (Tree a) (Tree a)
instance Functor Tree where fmap f (Leaf x) = Leaf (f x) fmap f (Branch l r) = Branch (fmap f l) (fmap f r)Applicative
Section titled “Applicative”Applicative sits between Functor and Monad in the type class hierarchy:
class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b (*>) :: f a -> f b -> f b (<*) :: f a -> f b -> f a
-- Laws:-- 1. Identity: pure id <*> v = v-- 2. Composition: pure (.) <*> u <*> v <*> w = u <*> (v <*> w)-- 3. Homomorphism: pure f <*> pure x = pure (f x)-- 4. Interchange: u <*> pure y = pure ($ y) <*> uApplicative Examples
Section titled “Applicative Examples”-- MaybeJust (+1) <*> Just 5 -- => Just 6Nothing <*> Just 5 -- => NothingJust (+1) <*> Nothing -- => Nothing
-- pure lifts a value into the applicativepure (+1) <*> Just 5 -- => Just 6-- This is the same as fmap (+1) (Just 5)
-- List: applies each function to each value[(+1), (*2)] <*> [1, 2, 3]-- => [(+1) 1, (+1) 2, (+1) 3, (*2) 1, (*2) 2, (*2) 3]-- => [2, 3, 4, 2, 4, 6]
-- IO: sequence actionspure print <*> getLine-- reads input then prints it
-- liftA2: convenience for two-argument functionsliftA2 (+) (Just 3) (Just 5) -- => Just 8liftA2 (+) [1, 2] [10, 20] -- => [11, 21, 12, 22]Applicative Style Programming
Section titled “Applicative Style Programming”-- Without ApplicativeaddMaybe :: Maybe Int -> Maybe Int -> Maybe IntaddMaybe (Just a) (Just b) = Just (a + b)addMaybe _ _ = Nothing
-- With ApplicativeaddMaybe :: Maybe Int -> Maybe Int -> Maybe IntaddMaybe = liftA2 (+)
-- Applicative style for validationdata Validation e a = Error e | Success a deriving (Show)
instance Functor (Validation e) where fmap f (Success x) = Success (f x) fmap _ (Error e) = Error e
instance Applicative (Validation e) where pure = Success Error e <*> _ = Error e _ <*> Error e = Error e Success f <*> Success x = Success (f x)The Monad Type Class
Section titled “The Monad Type Class”class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b -- "bind" (>>) :: m a -> m b -> m b -- "then" (discard first result) return :: a -> m a -- same as pure
-- Laws:-- 1. Left identity: return x >>= f = f x-- 2. Right identity: m >>= return = m-- 3. Associativity: (m >>= f) >>= g = m >>= (\x -> f x >>= g)Maybe as a Monad
Section titled “Maybe as a Monad”Maybe represents optional values. Binding propagates Nothing:
-- Manual Maybe handlingaddMaybes :: Maybe Int -> Maybe Int -> Maybe IntaddMaybes (Just a) (Just b) = Just (a + b)addMaybes _ _ = Nothing
-- With monadic bindaddMaybes :: Maybe Int -> Maybe Int -> Maybe IntaddMaybes ma mb = do a <- ma b <- mb return (a + b)
-- Chaining Maybe computationssafeDivide :: Int -> Int -> Maybe IntsafeDivide _ 0 = NothingsafeDivide x y = Just (x `div` y)
safeCompute :: Maybe IntsafeCompute = do a <- safeDivide 10 2 -- Just 5 b <- safeDivide 20 4 -- Just 5 return (a + b) -- Just 10
-- If any step returns Nothing, the whole computation is NothingsafeFail :: Maybe IntsafeFail = do a <- safeDivide 10 0 -- Nothing b <- safeDivide 20 4 -- never reached return (a + b) -- NothingEither as a Monad
Section titled “Either as a Monad”Either e represents computations that can fail with an error of type e:
-- Right is the success path, Left is the error pathsafeDivide :: Double -> Double -> Either String DoublesafeDivide _ 0 = Left "division by zero"safeDivide x y = Right (x / y)
compute :: Either String Doublecompute = do a <- safeDivide 10 2 b <- safeDivide 20 4 return (a + b)-- => Right 10.0
computeFail :: Either String DoublecomputeFail = do a <- safeDivide 10 0 b <- safeDivide 20 4 -- never reached return (a + b)-- => Left "division by zero"IO as a Monad
Section titled “IO as a Monad”The IO monad sequences I/O operations:
main :: IO ()main = do putStrLn "What is your name?" name <- getLine putStrLn "What is your age?" ageStr <- getLine let age = read ageStr putStrLn ("Hello, " ++ name ++ "! You are " ++ show age ++ " years old.")
-- Bind chains IO actionsgreetAndCount :: IO ()greetAndCount = do putStrLn "Enter words (empty line to quit):" go 0 where go n = do line <- getLine if null line then putStrLn ("Total: " ++ show n ++ " words") else go (n + 1)Do Notation
Section titled “Do Notation”Do notation is syntactic sugar for monadic bind (>>=) and >>:
-- Do notation:do x <- action1 y <- action2 action3 x y action4
-- Desugars to:action1 >>= \x -> action2 >>= \y -> action3 x y >> action4
-- Lines without <- are just sequencing with >>main :: IO ()main = do putStrLn "Hello" -- IO () putStrLn "World" -- IO ()-- desugars to: putStrLn "Hello" >> putStrLn "World"
-- let bindings in do notationmain :: IO ()main = do let x = 10 y = 20 putStrLn (show (x + y))Do Notation Desugaring Examples
Section titled “Do Notation Desugaring Examples”-- Example 1: Simple bindingdo a <- ma return (a + 1)-- => ma >>= \a -> return (a + 1)
-- Example 2: Multiple bindingsdo a <- ma b <- mb return (a + b)-- => ma >>= \a -> mb >>= \b -> return (a + b)
-- Example 3: Let bindingdo let x = 5 a <- ma return (x + a)-- => let x = 5 in ma >>= \a -> return (x + a)
-- Example 4: Last action is discardeddo a <- ma mb-- => ma >>= \_ -> mbCustom Type Classes
Section titled “Custom Type Classes”Defining a Type Class
Section titled “Defining a Type Class”-- A type class for things that can be serializedclass Serializable a where serialize :: a -> ByteString deserialize :: ByteString -> Maybe a
-- Instance for a custom typeinstance Serializable Person where serialize p = encode (personName p, personAge p) deserialize bs = case decode bs of Just (name, age) -> Just (Person name age) Nothing -> NothingDefault Method Implementations
Section titled “Default Method Implementations”class Eq a => Ord a where compare :: a -> a -> Ordering x < y = compare x y == LT x > y = compare x y == GT x <= y = compare x y /= GT x >= y = compare x y /= LT -- Only compare needs to be defined; the rest have defaultsMinimal Complete Definition
Section titled “Minimal Complete Definition”Each type class has a minimal complete definition — the smallest set of methods that must be implemented. The remaining methods have default implementations in terms of the required ones.
-- Eq minimal: either (==) or (/=)-- If you define (/=), (==) defaults to \x y -> not (x /= y)
-- Ord minimal: either compare or (<=)-- If you define compare, all comparison operators are derived
-- Show minimal: show-- Read minimal: readsPrecFoldable
Section titled “Foldable”Foldable generalizes folding to any container type:
class Foldable t where fold :: Monoid m => t m -> m foldMap :: Monoid m => (a -> m) -> t a -> m foldr :: (a -> b -> b) -> b -> t a -> b foldl :: (b -> a -> b) -> b -> t a -> b toList :: t a -> [a]
-- ExamplesfoldMap Sum [1, 2, 3] -- => Sum {getSum = 6}foldMap Product [1, 2, 3] -- => Product {getProduct = 6}foldr (+) 0 (Just 5) -- => 5foldr (+) 0 Nothing -- => 0toList (Just 3) -- => [3]Traversable
Section titled “Traversable”Traversable combines Functor and Foldable with the ability to apply an effectful function:
class (Functor t, Foldable t) => Traversable t where traverse :: Applicative f => (a -> f b) -> t a -> f (t b) sequenceA :: Applicative f => t (f a) -> f (t a)
-- traverse applies a function returning an applicative to each elementtraverse show [1, 2, 3] :: IO [String]-- In IO context, this could read values and convert them to strings
-- sequenceA flips: applies all effects, collecting resultssequenceA [Just 1, Just 2, Just 3] -- => Just [1, 2, 3]sequenceA [Just 1, Nothing, Just 3] -- => NothingsequenceA [putStrLn "a", putStrLn "b"] -- IO [(), ()] -- prints a, then bSemigroup and Monoid
Section titled “Semigroup and Monoid”Semigroup
Section titled “Semigroup”A semigroup is a set with an associative binary operation:
class Semigroup a where (<>) :: a -> a -> a
-- Associative law: (a <> b) <> c = a <> (b <> c)
instance Semigroup [a] where (<>) = (++) -- list concatenation
instance Semigroup String where (<>) = (++) -- string concatenation
instance Semigroup (Maybe a) where Nothing <> b = b a <> Nothing = a Just a <> Just b = Just (a <> b) -- requires Semigroup a
newtype Sum a = Sum { getSum :: a }instance Num a => Semigroup (Sum a) where Sum a <> Sum b = Sum (a + b)Monoid
Section titled “Monoid”A monoid is a semigroup with an identity element:
class Semigroup a => Monoid a where mempty :: a
-- Identity laws: mempty <> a = a, a <> mempty = a
instance Monoid [a] where mempty = []
instance Monoid String where mempty = ""
instance Monoid (Maybe a) where mempty = Nothing
newtype Product a = Product { getProduct :: a }instance Num a => Monoid (Product a) where mempty = Product 1 Product a <> Product b = Product (a * b)Using Monoids
Section titled “Using Monoids”-- mconcat folds a list with (<>)mconcat ["hello", " ", "world"] -- => "hello world"mconcat [Sum 1, Sum 2, Sum 3] -- => Sum 3
-- Using foldMap with MonoidfoldMap (Sum . length) ["hi", "hello", "world"]-- => Sum {getSum = 12}
-- Intercalate with monoidsintercalate ", " ["a", "b", "c"] -- => "a, b, c"Functor/Applicative/Monad Hierarchy
Section titled “Functor/Applicative/Monad Hierarchy”The relationship between these three type classes is:
Every Monad is an Applicative, and every Applicative is a Functor. However, there are useful types that are Functor or Applicative but not Monad:
-- All Monads are Functors and Applicatives-- But not all Functors are Applicative-- And not all Applicatives are Monads
-- ZipList is Applicative but not Monad-- newtype ZipList a = ZipList [a]
-- Validation is Applicative but not Monad (errors accumulate)-- data Validation e a = Error e | Success aKey Relationships
Section titled “Key Relationships”-- fmap can be expressed via Applicative:-- fmap f x = pure f <*> x
-- fmap can be expressed via Monad:-- fmap f x = x >>= return . f
-- pure and <*> can be expressed via Monad:-- pure x = return x-- mf <*> mx = mf >>= \f -> mx >>= \x -> return (f x)
-- But Functors and Applicatives can exist without Monad-- They are more general (less powerful but more widely applicable)Intuition
Section titled “Intuition”Type classes are skills badges, not family trees: In OOP, interfaces live inside the class hierarchy — you must declare “I implement this interface” at definition time. In Haskell, type classes are external skill badges: any type can earn the Eq badge, the Show badge, or the Ord badge, regardless of where it was defined or what its ancestors are. The compiler acts as the HR department, checking that every type used in a context has the right badges.
Why it matters: Type classes enable ad-hoc polymorphism — writing functions that work with any type that has a specific capability. You can write a sort function that works on any Ord type, and the compiler ensures only sortable types are passed in.
The key insight: Type classes separate what a type can do from what a type is. This is more flexible than inheritance because a type can gain capabilities after it’s defined, and the same type can implement multiple unrelated interfaces.
Practical Type Class Design
Section titled “Practical Type Class Design”When designing your own type classes, consider:
- Minimal complete definition: Provide defaults that make it easy to write instances
- Laws: Document what laws instances must satisfy
- Superclasses: Use superclasses to share functionality (e.g.,
OrdrequiresEq) - Avoid orphan instances: Define instances in the same module as the type or the class
- Consider newtype: Use newtype wrappers instead of orphan instances when possible
-- Good type class designclass Hashable a where hash :: a -> Int hashWithSalt :: Int -> a -> Int hashWithSalt salt x = salt `combine` hash x -- Minimal complete definition: hashCommon Mistakes
Section titled “Common Mistakes”Creating orphan instances: Defining an instance for a type class in a module that defines neither the type nor the class causes orphan instances. These can conflict with other instances and break the global consistency guarantee.
Confusing Foldable with Traversable: Foldable only consumes elements; Traversable applies an effectful function and preserves structure. Using foldMap when you need traverse loses the container shape.
Ignoring the monad laws: Breaking left identity, right identity, or associativity laws makes monadic code behave unpredictably. Relying on a monad that violates these laws leads to subtle bugs during refactoring.