The Monad
is one of the most infamous things around Haskell, and indeed functional programming, and so writing tutorials around them has become something of a cliche. Let me be clear - I really did try and avoid writing one, but it’s gotten to the point that it’s difficult to talk about the other more interesting stuff without at least mentioning it.
To try and avoid falling into the regular pitfalls, we are instead going to define the simplest possible Monad
, and then look at some examples of them in action. Hopefully you’ll see that Monads
aren’t that complicated or interesting - it’s actually the properties of the different datatypes that provide all the different behaviours and make it interesting.
Id
So. This is Id
.
newtype Id a
= Id { getId :: a}
It is a container that contains any value that we give it.
val :: Id Int
= Id 7 val
Which we can take out at any time with getId
.
plainVal :: Int
= getId val
plainVal -- plainVal = 7
Every Monad
must first be a Functor
, so let’s define that for Id
.
instance Functor Id where
fmap f (Id a) = Id (f a)
doubled :: Id Int
= fmap (\*2) val
doubled -- doubled == Id 14
It must also be an Applicative
.
instance Applicative Id where
pure = Id
Id f) <*> (Id a) = Id (f a)
(
idValue :: Id String
= pure "Hello!"
idValue -- idValue = Id "Hello!"
getLength :: Id Int
= Id length <\*> Id "Dogs"
getLength -- getLength == Id 4
Great stuff. Now we’ve got Functor
and Applicative
, we only need one more function to make a Monad
instance, and thats bind
(or >>=
). Bind
takes an Id a
value, and a function of signature (a -> Id b)
that takes the value inside, does something to it, and adds another layer of Id
. It then removes the extra layer, leaving us with Id b
. In some places it’s called flatMap
, as it maps over the value and then flattens it.
For Id
, it takes the value out, and then runs the function on it.
instance Monad Id where
Id m) >>= k = k m (
Pretty dull. Let’s plop a few bind
functions together. We’re using Do Notation, which is a way of making using Monad
values easier.
doubleAndWrap :: Int -> Id Int
doubleAndWrap i= pure (i \* 2)
-- doubleAndWrap 1 = Id 2
doubleAFewTimes :: Int -> Id Int
= do
doubleAFewTimes i <- doubleAndWrap i
j <- doubleAndWrap j
k <- doubleAndWrap k
l
doubleAndWrap l
-- doubleAFewTimes 10 = 160
OK! Not very interesting to be honest. Id
doesn’t really do anything other than make adding numbers up more confusing. Let’s look at other Monad
instances and try and see what they do.
Maybe
We’re going to use the Maybe
monad to chain together a head
-type function that returns the first item if it’s there.
firstItem :: [a] -> Maybe a
= Nothing
firstItem [] :\_) = Just a
firstItem (a
head3 :: [[[a]]] -> Maybe a
= do
head3 aaas <- firstItem aaas
aas <- firstItem aas
as <- firstItem as
a pure a
-- head3 [] -- Nothing
-- head3 [[[1,2,3]]] -- Just 1
Note that as soon as we get a Nothing
, the calculation stops, as such.
Either
We’re going to use Either
to do some string validation. Here is our error type:
data Error
= TooLong
| ContainsHorse
| IsEmpty
deriving (Show, Eq, Ord)
And here are a series of string validation functions that all return either Right String
if the string is OK, or Left Error
if not.
isEmpty :: String -> Either Error String
isEmpty s= if null s
then Left IsEmpty
else Right s
tooLong :: String -> Either Error String
tooLong s= if length s > 10
then Left TooLong
else Right s
containsHorse :: String -> Either Error String
containsHorse s= if "horse" `isInfixOf` s
then Left ContainsHorse
else Right s
This validate
function chains all the validations, returning either the String
or the first Error
.
validate :: String -> Either Error String
= do
validate s <- isEmpty s
t <- tooLong t
u
containsHorse u
-- validate "" == Left IsEmpty
-- validate "bah horse" == Left ContainsHorse
-- validate "really long string" == Left TooLong
-- validate "Hello" == Right "Hello"
List
The List
monad is interesting because the flattening effect of bind
means functions that turn values into more lists get flattened into one big list.
moreList :: Int -> [Int]
= [a - 1, a, a + 1]
moreList a -- moreList 1 == [0, 1, 2]
lotsMoreList :: Int -> [Int]
= do
lotsMoreList a <- moreList a
b
moreList b-- lotsMoreList 1 == [-1,0,1,0,1,2,1,2,3]
Reader
The Reader
monad is used to pass configuration around a program. Here we will define a type for Config
and a value for it.
data Config
= Config { ipAddress :: String
name :: String
,
}
config :: Config
= Config { ipAddress = "127.0.0.1"
config = "localhost"
, name }
These functions use the Config
value in the Reader
to make strings, using the ask
function to access the config.
printName :: Reader Config String
= do
printName <- ask
config pure ("the name is " <> name config)
printIp :: Reader Config String
= do
printIp <- ask
config pure ("The ip address is " <> ipAddress config)
The configReader
function shows how we can combine different Reader
instances with bind
configReader :: Reader Config String
= do
configReader <- printIp
ip <- printName
name pure (ip <> ", " <> name)
withConfig :: String
= runReader configReader config
withConfig -- withConfig == "The ip address is 127.0.0.1, the name is localhost"
Writer
The Writer
monad accumulates a log as it does computations, by using the tell
function to add items.
addOne :: Int -> Writer String Int
= do
addOne i "Add one "
tell pure (i + 1)
timesTwo :: Int -> Writer String Int
= do
timesTwo i "times two "
tell pure (i \* 2)
maths :: Int -> Writer String Int
= do
maths i <- addOne i
j <- timesTwo j
k pure k
-- runWriter maths 10 == (22, "Add one times two")
IO
The IO
monad is a very interesting one. Each time we run bind
, the value that comes out can be affected by stuff like user input, system time, or other side effects. This is how we can write effectful code in a relatively safe manner. This is how we write code like this that actually interacts with users.
things :: IO Unit
= do
things <- putStrLn "Hello! What is your name?"
\_ <- readLn
name putStrLn ("Hello, " ++ name)
That is all. I feel this somewhat rushes things a lot, and there is a lot to chew on, but hopefully these examples will help you build up an intuition for how these work.
Further Reading: