Mathcamp 2025
Glenn Sun
These slides may contain errors,
and should not be considered an authorative source.
"A monad is a monoid in the category of endofunctors."
"A monad is a monoid in the category of endofunctors."
____________ is ________________ for ___________________.
In Java, is List
a type?
No, but List<T>
is for any type T
.
In C++, is std::vector
a type?
No, but std::vector<T>
is for any type T
.
In Java, examples of type constructors include:
List
(which produces types List<T>
)Map
(which produces types Map<K, V>
)Set
(which produces types Set<T>
)In C++, examples of type constructors include:
std::vector
(which produces types std::vector<T>
)std::map
(which produces types std::map<K, V>
)std::set
(which produces types std::set<T>
)"A monad is a monoid in the category of endofunctors."
____________ is ________________ for type constructors.
Canonical example: lists
[[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
$\downarrow$
[1, 2, 3, 4, 5, 6, 7, 8, 9]
"A monad is a monoid in the category of endofunctors."
Flattening is ________________ for type constructors.
[1, 2, 3] ++ [4, 5, 6] == [1, 2, 3, 4, 5, 6]
"hello" ++ "world" == "helloworld"
3 ++ 5 == 8
"A monad is a monoid in the category of endofunctors."
Flattening is concatenation of type constructors.
Concatenation of lists has type signature: $$\pplus : \texttt{List<T>} \times \texttt{List<T>} \to \texttt{List<T>}$$
Concatenation of the type constructor List
has type signature: $$\pplus : \texttt{List<List<T>>}\to \texttt{List<T>} $$
This is flattening!
Up next: What are monads and why are they useful?
Later: More about categories and monoids
In your favorite language, are the following two pieces of code equivalent (ignoring differences in running time) for any function $f : \texttt{int} \to \texttt{int}$?
f(x) + f(x)
f(x) * 2
No, consider the following code.
public static int f(int x) {
System.out.println("User executed f");
return x + 1;
}
A pure function is deterministic and has no side effects.
Side effects change the state of the system outside the function.
Functional programming is a style that focuses on:
def odd_squares(arr):
return (arr.filter(lambda x: x % 2 == 1)
.map(lambda x: x ** 2))
Functional programming should feel like mathematics.
People like functional programming.
Some languages are designed for it: Excel, Mathematica, Haskell
How do these languages handle side effects?
Haskell uses mathematical notation for function definitions.
Function application does not require parentheses.
f :: Int -> Int
f x = x + 1
Just like in math, Haskell makes it easy to compose functions.
f :: Int -> Int
f x = x + 1
g :: Int -> Int
g x = x * 2
h :: Int -> Int
h = f . g
There is a built-in identity function.
It works with all types.
In Haskell, we usually write a
, b
, etc. for generic types.
id :: a -> a
id x = x
Haskell supports pattern matching in function definitions.
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)
(This code is not perfectly safe, it enters an infinite loop for negative values of n
.)
You can also define functions on multiple variables.
The type Int -> Int -> Int
means Int -> (Int -> Int)
.
gcd :: Int -> Int -> Int
gcd n 0 = n
gcd n m = gcd m (n `mod` m)
In other words, after this definition, Haskell considers gcd n
to be a perfectly good function with type Int -> Int
.
Pattern matching is especially powerful for algebraic data types.
data Shape = Circle Float | Rectangle Float Float
area :: Shape -> Float
area (Circle r) = pi * r * r
area (Rectangle l w) = l * w
In math, this is the type $\mathbb{R} + (\mathbb{R} \times \mathbb{R})$.
A List
is a recursive algebraic data type constructor.
data List a = Empty | Nonempty a (List a)
len :: List a -> Int
len Empty = 0
len (Nonempty x l) = 1 + len l
In math, we write $L(a) = 1 + (a \times L(a))$.
What happens when you execute the following in Java?
List<Integer> arr = new ArrayList<Integer>();
System.out.println(arr.get(0));
Throws an IndexOutOfBoundsException
.
List<Integer> arr = new ArrayList<Integer>();
System.out.println(arr.indexOf(6));
Prints -1
(a sentinel value).
Maybe
type constructor(Also called Optional
in other languages.)
data Maybe a = Nothing | Just a
peek :: List a -> Maybe a
peek Empty = Nothing
peek (Nonempty x l) = Just x
In other languages, we would likely have peek :: List a -> a
and it would either throw an exception or return a sentinel value.
Writer
type constructorWriter
allows you to collect text to print to the console.
data Writer a = Writer a String
addone :: Int -> Writer Int
addone x = Writer (x + 1) "User added one. "
Although it seems trivial right now (just a fancy name for a tuple), it will become important later.
IO
type constructorThis built-in type constructor is used whenever the console is used to get input or display output.
IO a
means an I/O operation that returns a value of type a
.
putStrLn :: String -> IO ()
getLine :: IO String
Note that getLine
is not a function!
We learned about the type constructors:
List
Maybe
Writer
IO
Secretly, these are all monads! That means we need to show how they implement flattening.
Next up: Different types that implement the same functions.
Type classes outline functionality that many types should share.
Eventually, we'll define the Monad
type class.
class Show a where
show :: a -> String
print :: Show a => a -> IO ()
print x = putStrLn (show x)
We can implement the functions in a type class for various types.
instance Show a => Show (List a) where
show Empty = ""
show (Nonempty x Empty) = show x
show (Nonempty x l) = show x ++ ", " ++ show l
main :: IO ()
main = print (Nonempty 3 (Nonempty 8 Empty))
We defined earlier that endofunctors are just type constructors.
Formally, an (endo)functor must also be an instance of the Functor
type class.
class Functor F where
fmap :: (a -> b) -> F a -> F b
The meaning of fmap f
is that it applies f
to each element inside the container.
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
f
❤️ x
f
😢x
fmap
🤝 f
x
f
❤️ x
instance Functor Writer where
fmap f (Writer x s) = Writer (f x) s
instance Functor List where
fmap f Empty = Empty
fmap f (Nonempty x l) = Nonempty (f x) (fmap f l)
fmap
is not enoughImagine that f
wants to change x
into y
and put it in a box, but x
is already in 2 boxes. One option is:
f
😭
x
fmap
🤝 f
x
f
😢
x
fmap
🤝 f
x
f
😈 x
y
But the next time, f
will have to get help from fmap
3 times!
A monad is a functor that also supports flattening. Formally,
class Functor m => Monad m where
join :: m (m a) -> m a
return :: a -> m a
Haskell prefers the word join
over flatten
, but the meaning is the same.
The function return
is less important, it just puts a value into the container.
Maybe
monad
instance Monad Maybe where
join (Just (Just x)) = Just x
-- Nothing cases omitted
return x = Just x
f
😭
x
join
✨
x
x
fmap
🤝 f
x
f
😈 x
y
In code, we can now do fmap f . join
.
Writer
monad
instance Monad Writer where
join (Writer (Writer x s1) s2) = Writer x (s1 ++ s2)
return x = Writer x ""
join
✨
x
📕
📗
x
📕 📗
Writer
monad
f :: Int -> Writer Int
f x = Writer (x + 1) "Applied f. "
g :: Int -> Writer Int
g x = Writer (x * 2) "Applied g. "
h :: Int -> Writer Int
h x = Writer (x - 1) "Applied h. "
We want the "composition" f . g . h
, but that is not possible.
Writer
monadInstead, we can use fmap
and join
!
composed :: Int -> Writer Int
composed = join . fmap f . join . fmap g . h
Haskell provides a reverse fish operator to make this more concise:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
f <=< g = join . fmap f . g
composed = f <=< g <=< h
Writer
monadReverse fish composition fulfills the same need as standard composition with side effects, but remains pure. Recall that:
f x = Writer (x + 1) "Applied f. "
g x = Writer (x * 2) "Applied g. "
h x = Writer (x - 1) "Applied h. "
Then f <=< g <=< h
is equivalent to:
composed x = Writer (((x - 1) * 2) + 1)
"Applied f. Applied g. Applied h. "
do
notationAnother way to use monads in Haskell is do
notation.
composed :: Int -> Writer Int
composed x = do
y1 <- h x
y2 <- g y1
y3 <- f y2
return y3
Superficially, it now looks a lot like other languages!
do
notationTo explain do
notation, you will will first need to understand lambda functions.
A lambda function is just an unnamed function. It has the following syntax:
squareAll :: List Int -> List Int
squareAll list = fmap (\x -> x * x) list
do
notationLet f :: a -> m a
for some Monad m
and type a
. Then,
g x = do
y <- f x
...
is defined to mean:
g = ((\y -> ...) <=< f)
do
notation
h x = do
y <- f x
z <- g y
return z
h = (\y -> ((\z -> return z) <=< g) y) <=< f
= (\y -> (return <=< g) y) <=< f
= return <=< g <=< f
= g <=< f
do
notation in I/O
main = do
print "Enter name:"
name <- getLine
print ("Hello, " ++ name)
main = ((\_ -> (
(\name ->
print ("Hello, " ++ name)
) <=< (\_ -> getLine)) ()
) <=< (\_ -> print "Enter name:")) ()
We learned about functors and monads.
fmap
helps functions jump into containersjoin
flattens out nested containers<=<
and do
make it easy to compose monadic functionsNext up: categories and monoids
Our category is called *
(all types). We said that an (endo)functor is a type constructor F :: * -> *
that supports fmap
.
Formally, fmap
must also satisfy the following:
fmap id == id
h == f . g
, thenfmap h == fmap f . fmap g
Monads are required to satisfy:
m a
):
join . return == id
x
📕
return
😈
x
📕
x
📕
join
✨
x
📕
x
📕
join . fmap return == id
Monads are required to satisfy:
m a
):
join . return == id
join . fmap return == id
x
📕
fmap
🤝 return
x
📕
return
😈 x
📕
x
📕
join
✨
x
📕
x
📕
m (m (m a))
): join . join == join . fmap join
x
📕
📗
📘
join
✨
x
📕
📗
📘
x
📕
📗 📘
join
✨
x
📕
📗 📘
x
📕 📗 📘
x
📕
📗
📘
fmap
🤝 join
x
📕
📗
📘
join
✨
x
📕
📗
📘
x
📕 📗
📘
join
✨
x
📕 📗
📘
x
📕 📗 📘
"Joining outer first is the same as joining inner first."
The monoid laws are exactly the same as the monad laws, with outside/inside replace with left/right!
class Monoid m where
(++) :: m -> m -> m
empty :: m
empty ++ x == x
andx ++ empty == x
(x ++ y) ++ z == x ++ (y ++ z)