Project 6: The Functor-Applicative-Monad Hierarchy
Project 6: The Functor-Applicative-Monad Hierarchy
Implement the Functor, Applicative, and Monad type class hierarchy from scratch for multiple types. Understand why each abstraction exists and prove the laws hold.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Expert |
| Time Estimate | 2-3 weeks |
| Language | Haskell (Alternatives: Scala, PureScript, OCaml) |
| Prerequisites | Project 5 (ADTs), understanding of type classes, higher-kinded types |
| Key Topics | Functors, Applicatives, Monads, Type Class Laws, Category Theory Connection |
| Main Resources | “Haskell Programming from First Principles” Chapters 16-18, Typeclassopedia |
1. Learning Objectives
By completing this project, you will:
- Understand the hierarchy conceptually: Explain why Functor, Applicative, and Monad form a progression and what each level adds
- Implement instances for multiple types: Write Functor, Applicative, and Monad instances for Maybe, Either, List, Reader, State, and IO
- Prove type class laws: Verify that your instances satisfy the required laws
- Distinguish Applicative from Monad: Explain precisely when you need Monad’s power and when Applicative suffices
- Compose effectful computations: Use the abstractions to sequence operations that might fail, carry state, or perform I/O
- Recognize the patterns: See where these abstractions appear in real-world code and other languages
- Understand the category theory connection: Know what functors and monads mean mathematically (at a conceptual level)
2. Conceptual Foundation
2.1 The Big Picture
Before diving into definitions, let’s understand what problem these abstractions solve.
The Core Problem: Computation in Context
Programs deal with values, but often those values come with “context”:
- A value that might not exist (Maybe)
- A value that might be an error (Either)
- A value that comes from configuration (Reader)
- A value that changes state (State)
- A value that requires I/O to produce (IO)
- Multiple possible values (List)
We want to work with these “values in context” while abstracting over what the context actually is.
The Hierarchy
Functor
|
| "You can map over me"
|
v
Applicative
|
| "You can apply wrapped functions to wrapped values"
|
v
Monad
|
| "You can sequence operations where later depends on earlier"
|
v
(More specialized abstractions...)
Each level adds capability:
- Functor: Transform the value inside, preserving structure
- Applicative: Combine independent contexts
- Monad: Chain dependent computations
2.2 Functor: Mapping Over Structure
The Intuition
A Functor is anything you can map over. If you have a value “in a box” and a function that transforms values, Functor lets you apply the function inside the box:
f :: a -> b
┌───┐ ┌───┐
│ a │ ───fmap f───> │ b │
└───┘ └───┘
Maybe Maybe
The structure (Maybe, List, IO, etc.) is preserved. Only the value inside changes.
The Definition
class Functor f where
fmap :: (a -> b) -> f a -> f b
Note that f is a type constructor (kind * -> *), not a concrete type. Maybe, [], IO are all type constructors.
The infix operator <$> is a synonym for fmap:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
The Laws
Functors must satisfy two laws:
Identity Law:
fmap id = id
Mapping the identity function changes nothing.
Composition Law:
fmap (f . g) = fmap f . fmap g
Mapping a composed function is the same as composing mapped functions.
These laws ensure fmap is “structure-preserving”–it only changes the values, not the shape.
Examples
-- Maybe
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
-- List
instance Functor [] where
fmap = map -- The familiar map function!
-- Either (fixed left type)
instance Functor (Either e) where
fmap _ (Left e) = Left e
fmap f (Right x) = Right (f x)
-- IO
instance Functor IO where
fmap f action = do
x <- action
return (f x)
Why Functor Matters
Functor captures the essence of “container-like” things. Once you implement fmap, you can:
-- Transform values in any Functor
increment :: Functor f => f Int -> f Int
increment = fmap (+1)
-- Works for Maybe, List, IO, Either, ...
increment (Just 5) -- Just 6
increment [1, 2, 3] -- [2, 3, 4]
increment (Right 10) -- Right 11
2.3 Applicative: Combining Contexts
The Limitation of Functor
Functor lets us apply a one-argument function. But what about multi-argument functions?
fmap (+) (Just 3) -- Just (3 +) :: Maybe (Int -> Int)
We get a function inside a context. Now we need to apply it to another value in a context. Functor can’t do this–it only handles a -> b, not f (a -> b).
The Intuition
Applicative lets us apply functions that are themselves in a context:
┌─────────┐ ┌───┐ ┌───┐
│ a -> b │ <*> │ a │ ───────> │ b │
└─────────┘ └───┘ └───┘
Maybe Maybe Maybe
This enables multi-argument functions in contexts:
pure (+) <*> Just 3 <*> Just 5 -- Just 8
The Definition
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
pure: Lift a value into the minimal context(<*>): Apply a wrapped function to a wrapped value (pronounced “ap” or “apply”)
Note: Applicative requires Functor. The hierarchy is: Functor => Applicative.
Alternative Formulation: liftA2
Applicative can also be defined with:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f x y = f <$> x <*> y
This lifts a binary function into an applicative context.
The Laws
Applicatives must satisfy four laws:
Identity:
pure id <*> v = v
Homomorphism (pure distributes over application):
pure f <*> pure x = pure (f x)
Interchange:
u <*> pure y = pure ($ y) <*> u
Composition:
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
There’s also a compatibility law with Functor:
fmap f x = pure f <*> x
Examples
-- Maybe
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
_ <*> Nothing = Nothing
Just f <*> Just x = Just (f x)
-- List (all combinations!)
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
-- Either
instance Applicative (Either e) where
pure = Right
Left e <*> _ = Left e
_ <*> Left e = Left e
Right f <*> Right x = Right (f x)
The Power of Applicative
-- Validate multiple independent things
data Person = Person String Int
-- Each validation is independent
validateName :: String -> Maybe String
validateAge :: Int -> Maybe Int
-- Combine with Applicative
validatePerson :: String -> Int -> Maybe Person
validatePerson name age = Person <$> validateName name <*> validateAge age
-- Or using liftA2:
validatePerson name age = liftA2 Person (validateName name) (validateAge age)
The key insight: the validations are independent. We don’t need the name to validate the age. This independence is what Applicative captures.
Applicative for Parallelism
Because Applicative computations are independent, they can potentially run in parallel:
-- Fetch two independent resources
fetchBoth :: IO (Data, Config)
fetchBoth = (,) <$> fetchData <*> fetchConfig
-- These could run concurrently!
Libraries like async exploit this for concurrent programming.
2.4 Monad: Dependent Sequencing
The Limitation of Applicative
Applicative can combine independent contexts, but sometimes later operations depend on earlier results:
-- We need the user's ID to look up their profile
lookupUser :: String -> Maybe User
lookupProfile :: User -> Maybe Profile
-- With Applicative, we can't express this dependency!
-- We need the User before we can call lookupProfile
The Intuition
Monad lets us sequence operations where each step can depend on the previous result:
┌───┐ ┌───┐
│ a │ ───(a -> m b)───────> │ b │
└───┘ └───┘
m m
The function a -> m b can inspect the a and decide what m b to produce. This is dependent computation.
The Definition
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b -- "bind"
return :: a -> m a -- Same as pure
The >>= operator (pronounced “bind”) is the heart of Monad. It:
- Extracts the value from the context
- Passes it to a function that produces a new context
- Returns that new context
Note: Monad requires Applicative (which requires Functor).
The Laws
Monads must satisfy three laws:
Left Identity:
return a >>= f = f a
Wrapping a value and immediately binding is the same as just calling the function.
Right Identity:
m >>= return = m
Binding with return changes nothing.
Associativity:
(m >>= f) >>= g = m >>= (\x -> f x >>= g)
Binding is associative (like function composition).
These ensure that sequencing behaves predictably.
Examples
-- Maybe
instance Monad Maybe where
Nothing >>= _ = Nothing
Just x >>= f = f x
-- List
instance Monad [] where
xs >>= f = concat (map f xs) -- Or: concatMap f xs
-- Either
instance Monad (Either e) where
Left e >>= _ = Left e
Right x >>= f = f x
Do-Notation
Haskell provides syntactic sugar for monadic code:
-- Without do-notation:
lookupBoth :: String -> Maybe Profile
lookupBoth name =
lookupUser name >>= \user ->
lookupProfile user >>= \profile ->
return profile
-- With do-notation:
lookupBoth :: String -> Maybe Profile
lookupBoth name = do
user <- lookupUser name
profile <- lookupProfile user
return profile
The <- arrow “extracts” the value from the monad for use in subsequent lines.
2.5 The Crucial Difference: Applicative vs Monad
This is the most important conceptual point to understand.
Applicative: Static Structure
With Applicative, the structure of the computation is fixed before running:
-- We know we'll run BOTH validateName AND validateAge
validatePerson name age = Person <$> validateName name <*> validateAge age
Even if validateName fails, we could (in principle) still run validateAge and collect both errors. The structure doesn’t depend on intermediate results.
Monad: Dynamic Structure
With Monad, later structure depends on earlier values:
-- We might NOT run lookupProfile if lookupUser fails or returns the wrong user
lookupBoth name = do
user <- lookupUser name
if userActive user
then lookupProfile user
else return defaultProfile
The if decision happens after extracting user. We can’t know which branch runs without actually running lookupUser.
Why This Matters
| Feature | Applicative | Monad |
|---|---|---|
| Structure | Known statically | Depends on values |
| Parallelism | Possible | Generally sequential |
| Error handling | Can collect all errors | Short-circuits |
| Power | Less | More |
| Optimization | More opportunities | Fewer opportunities |
Rule of thumb: Use Applicative when possible, Monad when necessary.
2.6 The Reader Monad
Reader represents computations that read from an environment.
Motivation
Many functions need configuration or context:
loadUser :: Config -> UserId -> User
loadSettings :: Config -> Settings
buildPage :: Config -> User -> Settings -> Page
Threading Config through every function is tedious. Reader abstracts this pattern.
Definition
newtype Reader r a = Reader { runReader :: r -> a }
instance Functor (Reader r) where
fmap f (Reader ra) = Reader $ \r -> f (ra r)
instance Applicative (Reader r) where
pure a = Reader $ \_ -> a
Reader rf <*> Reader ra = Reader $ \r -> rf r (ra r)
instance Monad (Reader r) where
Reader ra >>= f = Reader $ \r ->
let a = ra r
Reader rb = f a
in rb r
-- Ask for the environment
ask :: Reader r r
ask = Reader id
-- Run with a modified environment
local :: (r -> r) -> Reader r a -> Reader r a
local f (Reader ra) = Reader (ra . f)
Usage
type App a = Reader Config a
loadUser :: UserId -> App User
loadUser uid = do
config <- ask
return (fetchUser (dbConnection config) uid)
buildPage :: UserId -> App Page
buildPage uid = do
user <- loadUser uid
settings <- loadSettings
config <- ask
return (render config user settings)
-- Run the computation
main = do
let config = Config { ... }
let page = runReader (buildPage 123) config
print page
2.7 The State Monad
State represents computations that read and modify state.
Motivation
Some algorithms need mutable state:
-- Generate unique IDs
nextId :: State Int Int
nextId = do
n <- get
put (n + 1)
return n
Rather than threading state explicitly, State abstracts the pattern.
Definition
newtype State s a = State { runState :: s -> (a, s) }
instance Functor (State s) where
fmap f (State sa) = State $ \s ->
let (a, s') = sa s
in (f a, s')
instance Applicative (State s) where
pure a = State $ \s -> (a, s)
State sf <*> State sa = State $ \s ->
let (f, s') = sf s
(a, s'') = sa s'
in (f a, s'')
instance Monad (State s) where
State sa >>= f = State $ \s ->
let (a, s') = sa s
State sb = f a
in sb s'
-- Get the current state
get :: State s s
get = State $ \s -> (s, s)
-- Set the state
put :: s -> State s ()
put s = State $ \_ -> ((), s)
-- Modify the state
modify :: (s -> s) -> State s ()
modify f = State $ \s -> ((), f s)
Usage
-- A simple stack machine
type Stack a = [a]
type StackOp a = State (Stack a)
push :: a -> StackOp a ()
push x = modify (x:)
pop :: StackOp a a
pop = do
(x:xs) <- get
put xs
return x
-- Run a computation
example :: StackOp Int Int
example = do
push 1
push 2
push 3
a <- pop
b <- pop
return (a + b)
-- runState example [] == (5, [1])
2.8 The IO Monad
IO represents computations that interact with the outside world.
Why IO is a Monad
Haskell is pure–functions can’t have side effects. But programs need I/O! The solution: I/O actions are values of type IO a.
IO ais a description of an action that, when executed, produces ana- The Monad interface lets us sequence these descriptions
- The runtime system executes the resulting description
-- A value that describes printing "Hello"
putStrLn :: String -> IO ()
-- A value that describes reading a line
getLine :: IO String
-- Sequence actions
greet :: IO ()
greet = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
The main function has type IO (). The runtime executes this action.
Pure Functions in IO Context
-- Pure function
double :: Int -> Int
double x = x * 2
-- Use in IO context
main :: IO ()
main = do
putStrLn "Enter a number:"
line <- getLine
let n = read line :: Int -- Pure
let result = double n -- Pure
putStrLn ("Doubled: " ++ show result)
Mixing pure and impure code is natural with the Monad interface.
2.9 Category Theory Connection
You don’t need category theory to use monads, but understanding the connection deepens your understanding.
Functors in Category Theory
A functor is a structure-preserving map between categories:
- Maps objects to objects: Type
ato typeF a - Maps morphisms to morphisms: Function
a -> bto functionF a -> F b(that’sfmap!) - Preserves identity and composition (the Functor laws!)
In Haskell, we work in the category Hask (types as objects, functions as morphisms). A Functor instance defines an endofunctor on Hask.
Monads in Category Theory
A monad on a category C consists of:
- An endofunctor
T : C -> C - A natural transformation
eta : Id -> T(that’sreturn!) - A natural transformation
mu : T^2 -> T(that’sjoin!)
The monad laws in categorical form:
join . fmap join = join . join -- Associativity
join . fmap return = join . return = id -- Identity
The >>= form is equivalent:
m >>= f = join (fmap f m)
Why This Matters
Understanding the categorical view reveals:
- Monads are a very general pattern (not Haskell-specific)
- The laws ensure predictable composition
- Related concepts (natural transformations, adjunctions) provide more tools
2.10 Common Derived Operations
From the core operations, we derive many useful functions:
-- Sequence actions, discarding first result
(>>) :: Monad m => m a -> m b -> m b
m >> n = m >>= \_ -> n
-- Apply a function inside a monad
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
-- Lift a value
pure :: Applicative f => a -> f a
-- Join nested contexts
join :: Monad m => m (m a) -> m a
join mm = mm >>= id
-- Map then join
(=<<) :: Monad m => (a -> m b) -> m a -> m b
(=<<) = flip (>>=)
-- Kleisli composition
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \a -> f a >>= g
-- Sequence a list of actions
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)
-- Map then sequence
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
mapM f = sequence . fmap f
-- Filter with a monadic predicate
filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]
-- Fold with a monadic function
foldM :: Monad m => (b -> a -> m b) -> b -> [a] -> m b
3. Project Specification
3.1 Phase 1: Functor Instances (Days 1-3)
Implement Functor for all your types from Project 5.
Task 1: MyFunctor Class
class MyFunctor f where
myFmap :: (a -> b) -> f a -> f b
-- Infix synonym
(<$$>) :: MyFunctor f => (a -> b) -> f a -> f b
(<$$>) = myFmap
Task 2: Instances
instance MyFunctor MyMaybe where ...
instance MyFunctor (MyEither e) where ...
instance MyFunctor MyList where ...
instance MyFunctor MyTree where ...
instance MyFunctor ((->) r) where ... -- Reader without newtype
Task 3: Law Verification
Write property tests for the functor laws:
prop_functorIdentity :: (MyFunctor f, Eq (f a)) => f a -> Bool
prop_functorIdentity x = myFmap id x == x
prop_functorComposition :: (MyFunctor f, Eq (f c))
=> Fun a b -> Fun b c -> f a -> Bool
prop_functorComposition (Fn f) (Fn g) x =
myFmap (g . f) x == myFmap g (myFmap f x)
3.2 Phase 2: Applicative Instances (Days 4-7)
Task 4: MyApplicative Class
class MyFunctor f => MyApplicative f where
myPure :: a -> f a
(<**>) :: f (a -> b) -> f a -> f b
-- Derived operations
myLiftA2 :: MyApplicative f => (a -> b -> c) -> f a -> f b -> f c
myLiftA2 f x y = f <$$> x <**> y
myLiftA3 :: MyApplicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
myLiftA3 f x y z = f <$$> x <**> y <**> z
(*>>) :: MyApplicative f => f a -> f b -> f b
x *>> y = (const id) <$$> x <**> y
(<<*) :: MyApplicative f => f a -> f b -> f a
x <<* y = const <$$> x <**> y
Task 5: Instances
instance MyApplicative MyMaybe where ...
instance MyApplicative (MyEither e) where ...
instance MyApplicative MyList where ... -- All combinations!
instance MyApplicative ((->) r) where ... -- Reader
Task 6: ZipList Alternative
newtype MyZipList a = MyZipList { getMyZipList :: MyList a }
-- Applicative that zips rather than taking all combinations
instance MyApplicative MyZipList where ...
Task 7: Law Verification
prop_applicativeIdentity :: (MyApplicative f, Eq (f a)) => f a -> Bool
prop_applicativeIdentity v = (myPure id <**> v) == v
prop_applicativeHomomorphism :: (MyApplicative f, Eq (f b))
=> Fun a b -> a -> Bool
prop_applicativeHomomorphism (Fn f) x =
(myPure f <**> myPure x) == (myPure (f x) :: f b)
prop_applicativeInterchange :: (MyApplicative f, Eq (f b))
=> f (Fun a b) -> a -> Bool
prop_applicativeInterchange u y =
(u' <**> myPure y) == (myPure ($ y) <**> u')
where u' = fmap applyFun u
3.3 Phase 3: Monad Instances (Days 8-12)
Task 8: MyMonad Class
class MyApplicative m => MyMonad m where
(>>>=) :: m a -> (a -> m b) -> m b
myReturn :: a -> m a
myReturn = myPure
-- Derived operations
(>>>) :: MyMonad m => m a -> m b -> m b
m >>> n = m >>>= \_ -> n
myJoin :: MyMonad m => m (m a) -> m a
myJoin mm = mm >>>= id
(=<<<) :: MyMonad m => (a -> m b) -> m a -> m b
(=<<<) = flip (>>>=)
-- Kleisli composition
(>==>) :: MyMonad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >==> g = \a -> f a >>>= g
Task 9: Instances
instance MyMonad MyMaybe where ...
instance MyMonad (MyEither e) where ...
instance MyMonad MyList where ...
instance MyMonad ((->) r) where ... -- Reader
Task 10: Reader and State
newtype MyReader r a = MyReader { runMyReader :: r -> a }
newtype MyState s a = MyState { runMyState :: s -> (a, s) }
instance MyFunctor (MyReader r) where ...
instance MyApplicative (MyReader r) where ...
instance MyMonad (MyReader r) where ...
instance MyFunctor (MyState s) where ...
instance MyApplicative (MyState s) where ...
instance MyMonad (MyState s) where ...
-- Reader operations
myAsk :: MyReader r r
myLocal :: (r -> r) -> MyReader r a -> MyReader r a
-- State operations
myGet :: MyState s s
myPut :: s -> MyState s ()
myModify :: (s -> s) -> MyState s ()
Task 11: Law Verification
prop_monadLeftIdentity :: (MyMonad m, Eq (m b)) => a -> Fun a (m b) -> Bool
prop_monadLeftIdentity a (Fn f) = (myReturn a >>>= f) == f a
prop_monadRightIdentity :: (MyMonad m, Eq (m a)) => m a -> Bool
prop_monadRightIdentity m = (m >>>= myReturn) == m
prop_monadAssociativity :: (MyMonad m, Eq (m c))
=> m a -> Fun a (m b) -> Fun b (m c) -> Bool
prop_monadAssociativity m (Fn f) (Fn g) =
((m >>>= f) >>>= g) == (m >>>= (\x -> f x >>>= g))
3.4 Phase 4: Practical Applications (Days 13-14)
Task 12: Validation
-- Applicative that accumulates errors
newtype MyValidation e a = MyValidation (MyEither (MyList e) a)
-- Note: This is Applicative but NOT a valid Monad!
instance Semigroup e => MyApplicative (MyValidation e) where ...
-- Use case:
data PersonError = NameTooShort | AgeTooLow | EmailInvalid
validatePerson :: String -> Int -> String -> MyValidation PersonError Person
Task 13: Parser
newtype MyParser a = MyParser { runMyParser :: String -> MyMaybe (a, String) }
instance MyFunctor MyParser where ...
instance MyApplicative MyParser where ...
instance MyMonad MyParser where ...
-- Primitive parsers
myChar :: Char -> MyParser Char
myString :: String -> MyParser String
mySatisfy :: (Char -> Bool) -> MyParser Char
-- Combinators
myMany :: MyParser a -> MyParser (MyList a)
myMany1 :: MyParser a -> MyParser (MyList a)
mySepBy :: MyParser a -> MyParser sep -> MyParser (MyList a)
3.5 Deliverables
- Library modules with all type classes and instances
- Property tests for all laws
- Example applications (validation, parsing, state machine)
- Documentation with law proofs (can be informal)
4. Solution Architecture
4.1 Module Organization
src/
MyPrelude/
Functor.hs -- MyFunctor class and instances
Applicative.hs -- MyApplicative class and instances
Monad.hs -- MyMonad class and instances
Reader.hs -- MyReader type
State.hs -- MyState type
Validation.hs -- MyValidation type
Parser.hs -- MyParser type
MyPrelude.hs -- Re-exports
test/
Laws/
FunctorLaws.hs
ApplicativeLaws.hs
MonadLaws.hs
Instances/
MaybeSpec.hs
EitherSpec.hs
ListSpec.hs
ReaderSpec.hs
StateSpec.hs
examples/
ValidationExample.hs
ParserExample.hs
StateExample.hs
4.2 Implementation Order
- Functor first: It’s the simplest and required by others
- Applicative second: Requires Functor, required by Monad
- Monad third: The full power
- Derived operations: Build on the core
- Practical types: Reader, State, Validation, Parser
4.3 Key Implementation Insights
For List Applicative
-- All combinations (non-determinism)
instance MyApplicative MyList where
myPure x = MyCons x MyNil
fs <**> xs = myFlatMap (\f -> myFmap f xs) fs
For Reader
-- The key insight: a Reader is a function
instance MyMonad (MyReader r) where
MyReader ra >>>= f = MyReader $ \r ->
let a = ra r
MyReader rb = f a
in rb r
For State
-- The key insight: State threads the state through
instance MyMonad (MyState s) where
MyState sa >>>= f = MyState $ \s ->
let (a, s') = sa s
MyState sb = f a
in sb s'
5. Implementation Guide
5.1 Phase 1 Milestones
Milestone 1.1: MyFunctor class defined
- Class compiles
- Works with any
* -> *type
Milestone 1.2: Basic instances
- Maybe, Either, List work
- Laws verified by tests
Milestone 1.3: Function instance
(->) ras Functor (composition!)- Understand Reader pattern emerges
5.2 Phase 2 Milestones
Milestone 2.1: MyApplicative class
- Extends MyFunctor
- Both
pureand<*>defined
Milestone 2.2: Maybe and Either
- Short-circuit on Nothing/Left
- Laws verified
Milestone 2.3: List Applicative
- All combinations behavior
- Understand non-determinism interpretation
Milestone 2.4: ZipList
- Alternative Applicative for lists
- Demonstrate multiple valid instances
5.3 Phase 3 Milestones
Milestone 3.1: MyMonad class
- Extends MyApplicative
- Bind and return defined
Milestone 3.2: Core instances
- Maybe, Either, List complete
- Laws verified
Milestone 3.3: Reader Monad
- Wrap function type
- Implement ask, local
- Practical example
Milestone 3.4: State Monad
- Wrap state transformer
- Implement get, put, modify
- Stack machine example
5.4 Hints
Hint 1: Derive fmap from Applicative
myFmap f x = myPure f <**> x
This must be consistent! Verify with tests.
Hint 2: Derive (<*>) from Monad
mf <**> mx = mf >>>= \f -> mx >>>= \x -> myReturn (f x)
Again, must be consistent.
Hint 3: Join and Bind
They’re interderivable:
myJoin mm = mm >>>= id
m >>>= f = myJoin (myFmap f m)
Hint 4: Reader is just function
-- Reader r a is isomorphic to (r -> a)
-- ask = id
-- local f m = m . f
Hint 5: State threads state
Think of State s a as: “Give me a state, I’ll give you a value and a new state.”
-- State s a ~ (s -> (a, s))
-- get = \s -> (s, s)
-- put s' = \_ -> ((), s')
6. Testing Strategy
6.1 Law Testing with QuickCheck
-- Generic law tests parameterized by type
testFunctorLaws :: forall f a b c.
(MyFunctor f, Eq (f a), Eq (f c), Arbitrary (f a), Show (f a))
=> Proxy f -> Proxy a -> Proxy b -> Proxy c -> Spec
testFunctorLaws _ _ _ _ = describe "Functor laws" $ do
it "identity" $ property (prop_functorIdentity @f @a)
it "composition" $ property (prop_functorComposition @f @a @b @c)
6.2 Specific Instance Tests
describe "MyMaybe Monad" $ do
it "binds Just correctly" $
(MyJust 3 >>>= \x -> MyJust (x + 1)) `shouldBe` MyJust 4
it "binds Nothing correctly" $
(MyNothing >>>= \x -> MyJust (x + 1)) `shouldBe` MyNothing
it "sequences correctly" $
(MyJust 3 >>> MyJust 4) `shouldBe` MyJust 4
6.3 Integration Tests
describe "Reader monad" $ do
it "provides environment" $ do
let comp = myAsk >>>= \r -> myReturn (r + 1)
runMyReader comp 5 `shouldBe` 6
it "local modifies environment" $ do
let comp = myLocal (+10) myAsk
runMyReader comp 5 `shouldBe` 15
describe "State monad" $ do
it "threads state correctly" $ do
let comp = myGet >>>= \n -> myPut (n + 1) >>> myGet
runMyState comp 0 `shouldBe` (1, 1)
6.4 Property Tests for Kleisli Composition
prop_kleisliAssociative :: (MyMonad m, Eq (m d))
=> Fun a (m b) -> Fun b (m c) -> Fun c (m d) -> a -> Bool
prop_kleisliAssociative (Fn f) (Fn g) (Fn h) a =
((f >==> g) >==> h) a == (f >==> (g >==> h)) a
7. Common Pitfalls
7.1 Wrong Applicative for Either
Wrong (stops at first error):
instance MyApplicative (MyEither e) where
myPure = MyRight
MyLeft e <**> _ = MyLeft e -- Doesn't try second!
_ <**> MyLeft e = MyLeft e
MyRight f <**> MyRight x = MyRight (f x)
This is correct for Either but loses information. If you want to accumulate errors, use Validation.
7.2 Confusing Applicative and Monad
This needs Monad:
-- We need the result of the first to decide what to do
process :: MyMaybe Int -> MyMaybe Int
process mx = mx >>>= \x ->
if x > 0
then MyJust (x * 2)
else MyNothing
This only needs Applicative:
-- Both computations are independent
combine :: MyMaybe Int -> MyMaybe Int -> MyMaybe Int
combine mx my = (+) <$$> mx <**> my
7.3 State Threading Errors
Wrong (uses same state twice):
instance MyApplicative (MyState s) where
MyState sf <**> MyState sa = MyState $ \s ->
let (f, _) = sf s -- Ignores new state!
(a, s') = sa s -- Uses original state!
in (f a, s')
Right:
instance MyApplicative (MyState s) where
MyState sf <**> MyState sa = MyState $ \s ->
let (f, s') = sf s
(a, s'') = sa s' -- Uses updated state
in (f a, s'')
7.4 Reader vs State Confusion
- Reader: Read-only environment (configuration, dependencies)
- State: Mutable state (counters, accumulators)
If you find yourself wanting to “update” the Reader environment, you probably want State.
7.5 Forgetting the Laws
Instances that don’t satisfy the laws will behave unexpectedly:
-- If left identity fails:
return a >>= f /= f a
-- Code that relies on this equivalence will break!
Always verify your instances satisfy the laws.
8. Extensions and Challenges
8.1 Basic Extensions
- MonadFail: Handle pattern match failures
- Alternative/MonadPlus: Add choice (
<|>) and failure (empty) - MonadFix: Fixed points in monadic computations
8.2 Advanced Extensions
- Monad Transformers: Stack monads (covered in Project 9)
- Free Monads: Separate syntax from interpretation (covered in Project 12)
- Comonads: The dual of monads (
extend,extract) - Arrows: Generalization of functions with effects
8.3 Research Topics
- Effect Systems: Algebraic effects and handlers
- Selective Functors: Between Applicative and Monad
- Indexed Monads: State types that change
- Graded Monads: Track effect levels in types
9. Real-World Connections
9.1 Monads in Other Languages
| Language | Monad Support |
|---|---|
| Scala | for comprehensions, flatMap |
| F# | Computation expressions |
| OCaml | let* syntax (4.08+) |
| Rust | ? operator for Result, no general abstraction |
| JavaScript | Promise.then (monad-like, not lawful) |
| C# | LINQ (query syntax), async/await |
| Python | No built-in, libraries exist |
| Swift | Optional chaining (limited) |
9.2 Industry Patterns
- Error Handling: Either/Result for explicit errors (Rust, Haskell)
- Async: Promise/Future/Task (JS, Scala, C#)
- Dependency Injection: Reader pattern
- Configuration: Reader + Environment types
- Parsing: Parser combinators (parsec, megaparsec)
- State Machines: State monad pattern
9.3 When to Use What
| Situation | Abstraction |
|---|---|
| Transform values in containers | Functor |
| Combine independent effects | Applicative |
| Validate multiple things | Applicative + Validation |
| Chain dependent operations | Monad |
| Read-only configuration | Reader |
| Mutable state | State |
| Side effects | IO |
10. Interview Questions
After completing this project, you should be able to answer:
- What is a Functor and what are its laws?
- A type constructor with
fmap - Identity:
fmap id = id - Composition:
fmap (f . g) = fmap f . fmap g - Preserves structure while transforming contents
- A type constructor with
- What is the difference between Functor, Applicative, and Monad?
- Functor: map function over structure
- Applicative: apply wrapped function to wrapped value
- Monad: chain operations where later depends on earlier
- Each adds power; use the weakest sufficient
- When would you use Applicative over Monad?
- When computations are independent
- When you want to accumulate errors
- When you might want parallelism
- When the structure is known statically
- Explain the Reader monad.
- Represents computations that read from an environment
askretrieves the environmentlocalruns with a modified environment- Used for configuration, dependency injection
- Explain the State monad.
- Represents computations that modify state
getretrieves current stateputsets new state- Threads state through sequential operations
- What are the monad laws and why do they matter?
- Left identity:
return a >>= f = f a - Right identity:
m >>= return = m - Associativity:
(m >>= f) >>= g = m >>= (\x -> f x >>= g) - Ensure sequencing is predictable and refactoring is safe
- Left identity:
- How do Promises/async-await relate to monads?
- Promise.then is like bind
- async/await is like do-notation
- Not lawful (exceptions, eager evaluation)
- Same intuition: sequencing effectful operations
11. Self-Assessment Checklist
Functor
- Can implement Functor for any container type
- Can state and verify the Functor laws
- Understand
(->) ras a Functor - Can use
<$>fluently
Applicative
- Can implement Applicative for Maybe, Either, List
- Understand the difference between List and ZipList
- Can use
<*>andliftA2fluently - Can explain when Applicative suffices over Monad
Monad
- Can implement Monad for Maybe, Either, List
- Can state and verify the Monad laws
- Understand the relationship between
>>=andjoin - Can use do-notation effectively
Reader and State
- Can implement Reader from scratch
- Can implement State from scratch
- Know when to use each
- Can use
ask,local,get,put
Laws and Theory
- Can prove instances satisfy laws (informally)
- Understand why laws matter for refactoring
- Can explain the categorical perspective (high level)
- Understand Kleisli composition
Practical Application
- Can build a Validation Applicative
- Can build a Parser Monad
- Can refactor code to use these abstractions
- Know when to use which abstraction
12. Resources
Primary Texts
- “Haskell Programming from First Principles” by Allen & Moronuki
- Chapter 16: Functor
- Chapter 17: Applicative
- Chapter 18: Monad
- Excellent exercises, thorough explanations
- “Programming in Haskell” by Graham Hutton
- Chapter 12: Monads and More
- Concise and mathematical
- “Learn You a Haskell for Great Good!” by Miran Lipovaca
- Chapter 11: Functors, Applicatives, and Monoids
- Chapter 12: A Fistful of Monads
- Visual and fun
Essential Online Resources
- Typeclassopedia
- Comprehensive guide to Haskell type classes
- The definitive reference
- All About Monads
- Tutorial with many examples
- Good for building intuition
- Functors, Applicatives, and Monads in Pictures
- Visual explanations
- Great for intuition
Papers
- “Applicative Programming with Effects” by McBride and Paterson
- The original Applicative paper
- Introduces idiom brackets (Applicative syntax)
- “Monads for Functional Programming” by Philip Wadler
- Classic introduction to monads
- Excellent examples
- “The Essence of the Iterator Pattern” by Gibbons and Oliveira
- Relates iterators to Applicative/Traversable
- Deep connections
Category Theory (Optional)
- “Category Theory for Programmers” by Bartosz Milewski
- Free online book
- Connects theory to programming
- nLab
- Rigorous mathematical definitions
- For those who want the full picture
Video Lectures
- Haskell MOOC (University of Helsinki)
- Weeks 4-5 cover these topics
- Exercises included
- Bartosz Milewski’s Category Theory (YouTube)
- 3-part lecture series
- Excellent for categorical perspective
“Once you understand monads, you lose the ability to explain them to others.” – The Monad Tutorial Fallacy
But that’s okay. After this project, you won’t need an explanation–you’ll have built them yourself.