Monad Lecture 2024-02-05
Table of Contents
- 1. Resources
- 2. Tasks
[2/2]
- 3. Brainstorm
- 4. Learning Outcomes
- 5. Outline (Draft 1)
See also: Understanding Monads
1. Resources
2. Tasks [2/2]
2.1. DONE How can I incorporate questions and interaction into this lesson?
2.2. DONE Build the slides
3. Brainstorm
Start with:
- “Going from imprecise but accurate to precise and accurate”
- Monads are interfaces
- “wrap” and “thread”
- example: Maybe
- why do we care? generic interface
- Monads as effects
- What are effects?
- How do Monads model effects?
- More effects with Monads to handle them
- List and non-determinism
- Reader/Writer
- Digression: typeclasses vs. interfaces
- More monads
- State
- Useful monad functions
- Combining monads
- Alternatives to monads for handling effects
4. Learning Outcomes
- Answer: what is a monad? (type +
return
+bind
) - Answer: why do we have monads? (handle effects cleanly)
- Answer: what are common monads? (Maybe, Either, List/Nondeterminism, IO)
- Know: you can have more than
return
andbind
operating on monads - Know:
do
notation is syntactic sugar
Stretch goals:
- Know: difference between an interface and a typeclass
- Know: how to handle effects in combination ← big stretch!
5. Outline (Draft 1)
5.1. What is a monad?
Caveat: I will be moving from an imprecise, intuitive, and accurate definition of a monad to a more precise, rigorous, and more accurate definition. Before you skewer me, be patient.
Monads, for all the mystery surrounding them, are really quite simple. If you’re thinking, “there’s got to be more to it than that”, well, not really. It’s just that the myriad applications of this simple thing is what is surprising.
5.1.1. Monad as an interface
A monad is an interface.
public interface Monad<T> { <R> Monad<R> thread(Function<T, Monad<R>> f); Monad<T> wrap(T value); }
class Monad m where thread :: m a -> (a -> m b) -> m b wrap :: a -> m a
Any type can become a monad, provided it implements this interface.
data Foo a = Foo a deriving (Show) instance Functor Foo where fmap f (Foo a) = Foo $ f a instance Applicative Foo where pure = Foo (Foo a) <*> x = fmap a x instance Monad Foo where return = pure Foo x >>= f = f x
Turns out, monads are also functors and applicatives—never mind—just know that they also need to implement fmap
and <*>
as well.
5.1.2. What are applicative functors?
Let’s start with the functor part: a functor is a fancy name for a container that you can map
over. In Haskell land, we call this function fmap
, but you can think of just implementing map
.
What are applicatives? These generalize function application to work with things other than functions. You need two functions: pure
, which just takes a value and wraps it in the data type, and apply
or <*>
.
5.2. Why do we have monads?
Monads are a handy way of modeling effects.
What? I don’t see how that from the interface.
Of course not—let’s look at an example.
5.2.1. What is an effect?
An effect is any observable behavior other than the return value.
Question: What are some effects?
- failure/exception
- IO
- modifying state
Question: Why do you think effects might be something we want to worry about?
- Breaks local reasoning
- Side-effect-free functions are easier to test and compose
- Easier to reason about under concurrency
- Optimizations available
- etc.
5.2.2. Encoding effects
Monads let us encode effects; let’s take a look at an example
5.2.3. Example: encoding failure with Maybe
data Maybe a = Nothing | Just a instance Monad Maybe where return = Just Nothing >>= f = Nothing (Just x) >>= f = f x instance MonadFail Maybe where fail _ = Nothing instance MonadPlus Maybe where mzero = Nothing Nothing `mplus` x = x x `mplus` _ = x
Monads are also useful to restrict access to a value outside a particular context.
Question: What have we gained by implementing Maybe
?
5.3. What are some common monads?
- Maybe
- Either
- State
- Writer
- Reader
5.4. What is a monad really?
5.4.1. Typeclasses vs Interfaces
Implementing a generic return
is a little tricker if all we have are interfaces.
5.4.2. Coping in languages without proper typeclasses
We can use custom constructors—not as nice as return
because we have to know exactly what monad it is we are operating in.
Rust doesn’t have typeclasses, but that doesn’t mean we can’t have Monads. Result
and Option
are monads!
Fun fact, Java’s optional breaks the monad laws by not being able to have null
inside an optional.
Second option: delay wrapping. Racket’s monad library works by delaying what functor it uses with pure
. From the docs:
Lifts a plain value into an applicative functor. When initially called, this function simply places the value in a box because it cannot yet know what kind of functor it needs to produce. When used, the value will be coerced into a functor of the appropriate type using the relevant value’s
pure
method.
5.5. Using monads comfortably
5.5.1. The do
notation
This:
do x <- Just 42 y <- Just (x + 1) Just (y * 2)
is the same as this:
Just 42 >>= (\x -> (Just (x + 1)) >>= (\y -> Just (y * 2)))
5.5.2. Other helpful functions
liftM
fmap
<*>
(from the Applicative type class)(Monad m) => m (m a) → m a
join (fmap f m)
≡m >>= f
filterM
mapM
foldM
liftM :: Monad m => (a1 -> r) -> m a1 -> m r fmap :: Functor f => (a -> b) -> f a -> f b (<*>) :: Applicative f => f (a -> b) -> f a -> f b join :: Monad m => m (m a) -> m a mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b) filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a] foldM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
5.6. More advanced macro example: nondeterminism
Walk through how allSolutions
works.
5.7. Practice: state and random number generation
Let’s write a linear congruence generator
\[ X_{n+1} = (aX_n + c) \mod m \]
where
- \(m\), \(0 < m\)
- the modulus
- \(a\), \(0 < a < m\)
- the multiplier
- \(c\), \(0 \leq c < m\)
- the increment
- \(X_0\), \(0 \leq X_0 < m\)
- the seed or start value
are integer constants that specify the generator.
linearCongruence :: Int -> Int linearCongruence seed = (75 * seed + 74) `mod` (2^16 + 1) threeFlips :: Seed -> (Bool, Bool, Bool) threeFlips s = let (firstCoin, s') = randCoin s (secondCoin, s'') = randCoin s' (thirdCoin, s''') = randCoin s'' in (firstCoin, secondCoin, thirdCoin)
But it’s cumbersome to thread the state of the random number generator. We can use a monad to make it more convenient.
First, the type of the state monad is s -> (a, s)
, where s
is some state, and a
is the result of running the stateful computation.
5.8. Monad laws
These are important to know if you make your own monad. They are simple and fairly straight-forward, but they will help you determine how your monad operates.
These are not enforced by the compiler!
How return
and bind
interact:
return x >>= f -- is the same as f x x >>= return -- is the same as x
How bind
chains:
x >>= f >>= g -- explicitly parenthesized is (x >>= f) >>= g -- which is the same as x >>= (\y -> (f y) >>= g)