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) * 2No, 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:
ListMaybeWriterIOSecretly, 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 ❤️ xf 😢xfmap 🤝 fxf ❤️ 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 == idh == f . g, thenfmap h == fmap f . fmap gMonads are required to satisfy:
m a):
join . return == idx 📕
return 😈
x 📕
x 📕
join ✨
x 📕
x 📕
join . fmap return == idMonads are required to satisfy:
m a):
join . return == idjoin . fmap return == idx 📕
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)