Lab 10. Functors & Monads
10.0. Understanding Functors and Monads
Functor
The Functor typeclass represents the mathematical functor: a mapping between categories.
In Haskell, Functors are defined as follows:
class Functor m where fmap :: (a -> b) -> f a -> f b
map
?
map :: (a -> b) -> [a] -> [b]
This is a specific implementation of fmap
, that exists in Haskell for historic reasons, a list also has a Functor
instance defined, with fmap = map
We can declare a Functor
instance for any abstract type f a
which has the ability to 'map' over it's value, while preserving the structure of f
.
Declaring Functor
instance for f
, allows us to use functions relating to mapping for any type f a
.
For fmap
to have a predictible behaviour, any Functor
instance we define needs to obey the Functor Laws:
-- Functors must preserve identity morphisms fmap id = id -- Functors preserve composition of morphisms fmap (f . g) = fmap f . fmap g
Monad
A monad is an algebraic structure used to describe computations as sequences of steps, and to handle side effects such as state and IO. They also provide a clean way to structure our programs.
In Haskell, Monads are defined as follows:
class Monad m where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b
For monads to behave correctly, a monad instance needs to obey the Monad Laws:
-- left identity return a >>= h = h a -- right identity m >>= return = m -- associativity (m >>= g) >>= h = m >>= (\x -> g x >>= h)
NOTE: any Monad instance has a corresponding Functor and Applicative (we will not discuss applicatives for this course, but Haskell requires it).
If you defined a Monad instance for a class Foo
, you can define the Functor and Applicative as follows:
Do notation
You can probably notice a lot of monadic code uses binds and lambda functions. Take for example the following snippet that uses monads to unpack 2 Maybe
values and add them.
add ma mb = ma >>= (\a -> mb >>= (\b -> return (a + b)))
Haskell provides do-notation, which is a syntactic sugar for this kind of expressions (also called hanging lambdas).
add ma mb = do a <- ma b <- mb return (a + b)
10.1. Working with 'Maybe'
Maybe
is already defined as a Functor and Monad:
instance Functor Maybe where fmap f x = case x of Just v -> Just (f v) Nothing -> Nothing instance Monad Maybe where return = Just mx >>= f = case mx of Just v -> f v Nothing -> Nothing
This section is meant to accommodate you to using Functors and Monads by playing around with an already implemented and familiar Monad: Maybe.
10.1.1. Implement a function add5
that adds 5 to a Maybe Int
, use fmap
instead of case
or pattern matching.
add5 :: Maybe Int -> Maybe Int
10.1.2. Implement functions add
, sub
and mult
that add, substract and multiply Maybe Int
, use do
notation instead of case
or pattern matching.
add :: Maybe Int -> Maybe Int -> Maybe Int sub :: Maybe Int -> Maybe Int -> Maybe Int mult :: Maybe Int -> Maybe Int -> Maybe Int
10.2. Working with IO
We can finally learn about IO in Haskell. Because it's a pure functional language, where we have no side-effects, IO is not really possible, reading external values or printing values is inherently a side-effect. Because of this, IO is defined as a Monad, where the context is the external world.
Very simply put, something with type IO a
is a value of type a
coming from the external world. When we print something, the type is simply IO ()
, because there is no value coming from the external world, we just interact with it.
Let's look at the type annotations of some usual IO functions:
print :: Show a => a -> IO () putStrLn :: String -> IO () getLine :: IO String
The main function in Haskell has the following type annotation:
main :: IO ()
This means a main
function is just a function that interacts with the external world.
You can use the commandline utility runhaskell main.hs
to quickly test your Haskell programs without compiling them.
10.2.1. Write a Hello World
program and execute it.
10.2.2. Write a program that reads a value n
and prints the n-th
fibonacci number.
read
to convert from strings to certain types (is the opposite of show
).
10.2.3. Write a program that reads a value n
, a vector of n
numbers and prints them in sorted order.
IO
is also a Functor
, we can fmap
over yet 'unread' values.
10.2.4. Write a program that reads a value n
, and prints the sequences given by the Collatz conjecture. If the current number x
is even, the next number is x / 2
, if x
is odd the next number is 3 * x + 1
, the sequence stops when you hit the number 1.
10.3. Lists
Let's take a moment and think about lists, we already experimented with Functors over lists a lot. It's simply the map
function we are extremely familiar with, but what would mean to sequence 2 lists using $(>>=)$?
Hint: We actually have already seen this behaviour before while using list comprehensions.
What is the context a list puts a value in? Non-determinism, a list represent the fact that the object can have multiple possible values.
We will take a simple example where the list monad might be very helpful. Conside a 8×8 chess board. A position would be:
type Pos = (Int, Int)
10.3.1. Make a function that given a position, tells you where a Knight might be able to move ( how a Knight moves).
moveKnight :: Pos -> [Pos] moveKnight (x, y) = do ???
10.3.2. Make a function that given a start position and a target position, tells you if a Knight can get to the target position in exactly 3 moves.
canReachIn3 :: Pos -> Pos -> Bool canReachIn3 = undefined
10.3.3.(* * *) Make a function that given a number k
, a start position and a target position, tell you if a Knight can get to the target position in exactly k
moves.
canReachInK :: Int -> Pos -> Pos -> Bool canReachInK = undefined
10.4. Probability Distributions (M3 flashbacks)
Let's also try to define our own Monads. Keep in mind that we should never have the goal of making something a monad, rather we should define types that model aspects of our problem, and if we notice that they represent types with context that can be abstracted using monads, use them for simplicity.
One such type is a Probability Distribution, we can define it as a list of pairs of value and probability.
newtype Prob a = Prob [(a, Float)] deriving Show
10.4.1. Define a Functor
instance for Prob
, mapping over a probability should change the value, but not affect the probability. Check that the functor laws apply.
instance Functor Prob where fmap = undefined prob :: Prob Int prob = Prob [(1, 0.5), (2, 0.25), (3, 0.25)] -- fmap (+3) prob = Prob [(4, 0.5), (5, 0.25), (6, 0.25)]
10.4.2. Define a auxiliary function flatten
that takes a probability of probabilities (Prob (Prob a)
) and returns a probability (Prob a
).
flatten :: Prob (Prob a) -> Prob a flatten = undefined nested_prob :: Prob (Prob Int) nested_prob = Prob [(prob, 0.7), (prob, 0.3)] -- flatten nested_prob = Prob [(1,0.35),(2,0.175),(3,0.175),(1,0.15),(2,7.5e-2),(3,7.5e-2)]
10.4.3. Using flatten
, define a Monad
instance for Prob
(it should behave in a similar fashion with the List monad, but also keep the probability context). Check that the monad laws apply.
instance Monad Prob where return x = undefined m >>= f = undefined -- Monad expects Applicative to be declared as well, take this as it is instance Applicative Prob where pure = return pf <*> px = do f <- pf x <- px return (f x)
Let's play around with our monad, let's define a coin:
data Coin = Heads | Tails deriving Show coin :: Prob Coin coin = Prob [(Heads, 0.5), (Tails, 0.5)] unfair_coin :: Prob Coin unfair_coin = Prob [(Heads, 0.6), (Tails, 0.4)] flip :: Prob [Coin] flip = do x <- coin y <- coin z <- coin return [x, y, z]
10.4.4. Make a function that returns the probability distribution of a fair n-sided die.
die :: Int -> Prob Int die n = undefined -- (,) <$> (die 20) <*> (die 6) -- the probability distribution of rolling a d20 followed by a d6
10.4.5. Let's use this framework to solve a M3 problem: “Jo has took a test for a disease. The result of the test is either positive or negative and the test is 95% reliable: in 95% of cases of people who really have the disease, a positive result is returned, and in 95% of cases of people who do not have the disease, a negative result is obtained. 1% of people of Jo’s age and background have the disease. Jo took the test, and the result is positive. What is the probability that Jo has the disease?”