Type Classes | Haskell - Wyatt's Notes
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)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: hashflowchart TD
A[1_Type Classes] --> B[Key Concepts]
A --> C[Core Principles]
A --> D[Practical Applications]
B --> E[Fundamental definitions]
C --> F[Design patterns]
D --> G[Real-world usage]Intuition
Section titled “Intuition”Type classes are like interfaces in other languages, but more flexible. A type class defines a set of behaviors (like Show or Eq), and any type that implements those behaviors can participate. This is like a club membership: the club defines the rules, and any type that follows the rules can join.
The difference between type classes and interfaces is that type classes are separate from the types themselves. A type can gain new abilities by being made an instance of a type class after it is defined. This is like getting a new skill certification after you have already started your career: your abilities grow without changing who you are.
Worked Examples
Section titled “Worked Examples”Example 1: Custom Functor and Monad for a Parser
Section titled “Example 1: Custom Functor and Monad for a Parser”Problem: Implement a simple parser type that is a Functor, Applicative, and Monad, then use it to parse simple expressions.
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
instance Functor Parser where fmap f (Parser p) = Parser $ \input -> case p input of Just (result, rest) -> Just (f result, rest) Nothing -> Nothing
instance Applicative Parser where pure x = Parser $ \input -> Just (x, input) Parser pf <*> Parser pa = Parser $ \input -> case pf input of Just (f, rest1) -> case pa rest1 of Just (a, rest2) -> Just (f a, rest2) Nothing -> Nothing Nothing -> Nothing
instance Monad Parser where Parser pa >>= f = Parser $ \input -> case pa input of Just (a, rest) -> runParser (f a) rest Nothing -> Nothing
-- Parser combinatorschar :: Char -> Parser Charchar c = Parser $ \input -> case input of (x:xs) | x == c -> Just (c, xs) _ -> Nothing
digit :: Parser Chardigit = Parser $ \input -> case input of (x:xs) | x >= '0' && x <= '9' -> Just (x, xs) _ -> Nothing
string :: String -> Parser Stringstring [] = pure []string (c:cs) = (:) <$> char c <*> string cs
-- UsageparseHello :: Parser StringparseHello = string "hello"
-- runParser parseHello "hello world" => Just ("hello", " world")-- runParser parseHello "goodbye" => NothingExplanation: The Parser type wraps a function from input to a possible result and remaining input. Functor lets us transform the parsed value. Applicative lets us apply parsed functions to parsed values. Monad lets us chain parsers where each depends on the previous result. This is the foundation of parser combinator libraries.
Example 2: Monad Transformers for Stacking Effects
Section titled “Example 2: Monad Transformers for Stacking Effects”Problem: Use MaybeT to combine Maybe and IO effects in a clean way.
import Control.Monad.Trans.Maybeimport Control.Monad.Trans.Class (lift)
-- Lookup a user by ID in a simulated databaselookupUser :: Int -> IO (Maybe String)lookupUser 1 = pure (Just "Alice")lookupUser 2 = pure (Just "Bob")lookupUser _ = pure Nothing
-- Lookup an email by usernamelookupEmail :: String -> IO (Maybe String)lookupEmail "Alice" = pure (Just "alice@example.com")lookupEmail "Bob" = pure (Just "bob@example.com")lookupEmail _ = pure Nothing
-- Combined lookup using MaybeTgetUserEmail :: Int -> MaybeT IO StringgetUserEmail userId = do username <- MaybeT (lookupUser userId) email <- MaybeT (lookupEmail username) return email
-- Run the computationmain :: IO ()main = do result <- runMaybeT (getUserEmail 1) case result of Just email -> putStrLn $ "Email: " ++ email Nothing -> putStrLn "User or email not found"
-- Email: alice@example.comExplanation: MaybeT IO stacks Maybe on top of IO. The do notation sequences operations that may fail at each step. If any MaybeT action returns Nothing, the entire computation short-circuits. lift would be used to embed plain IO actions inside MaybeT IO.
Example 3: Type Class for Deserialisation
Section titled “Example 3: Type Class for Deserialisation”Problem: Define a FromConfig type class that extracts values from a configuration map, with instances for basic types.
import qualified Data.Map.Strict as Map
class FromConfig a where fromConfig :: Map.Map String String -> String -> Either String a
instance FromConfig String where fromConfig cfg key = case Map.lookup key cfg of Just val -> Right val Nothing -> Left $ "Key not found: " ++ key
instance FromConfig Int where fromConfig cfg key = case Map.lookup key cfg of Just val -> case reads val of [(n, "")] -> Right n _ -> Left $ "Invalid integer for key: " ++ key Nothing -> Left $ "Key not found: " ++ key
instance FromConfig Bool where fromConfig cfg key = case Map.lookup key cfg of Just "true" -> Right True Just "false" -> Right False Just val -> Left $ "Invalid boolean for key " ++ key ++ ": " ++ val Nothing -> Left $ "Key not found: " ++ key
-- Usageconfig :: Map.Map String Stringconfig = Map.fromList [ ("host", "localhost") , ("port", "8080") , ("debug", "true") ]
getHost :: Either String StringgetHost = fromConfig config "host" -- Right "localhost"
getPort :: Either String IntgetPort = fromConfig config "port" -- Right 8080
getDebug :: Either String BoolgetDebug = fromConfig config "debug" -- Right TrueExplanation: The FromConfig type class defines how to extract a value of type a from a Map String String. Each instance implements the conversion for its type. reads is used for Int parsing (returns a list of successful parses). The Either String a return type provides explicit error messages.
Common Mistakes
Section titled “Common Mistakes”Confusing type class instances with type class definitions. A type class defines an interface (like Eq), while an instance makes a specific type implement that interface (like instance Eq Bool). Students often mix up the class keyword (which defines the interface) with the instance keyword (which provides an implementation).
Using show on values without a Show instance. The show function requires the Show type class. If a type does not derive or implement Show, calling show on it produces a compilation error. Always check that the type has a Show instance before using show or print.
Forgetting that type class constraints propagate through function signatures. Writing f :: Eq a => a -> a -> Bool means the function works for any type with an Eq instance. Students sometimes omit the constraint, then wonder why the compiler complains about == being undefined for their type.
Cross-References
Section titled “Cross-References”- Types and Functions - How parametric polymorphism works alongside type class constraints
- Monads and Functors - How Functor, Applicative, and Monad form a type class hierarchy
- Advanced Types - Higher-kinded types and type families that extend type class programming