15 March 2022
To clarify from the start, this article is meant for people who write Haskell, because it covers
do notation, the most common way of writing monadic code. I wanted to write it, because I believe you can write monadic code without understanding monads.
Instead, you need to know 2 things:
IO, we are talking about functions such as
In this article, I will cover the rules.
The examples below mostly use the
IO monad, but the rules apply to all monads.
You can't use functions that return different monads in the same
We have these 2 functions:
getArgs :: IO [String] headMaybe :: [a] -> Maybe a
This won't work:
main = do cliArgs <- getArgs firstArg <- headMaybe cliArgs
it doesn't work, because
getArgs returns an
IO value, while
headMaybe returns a
Maybe value. Keep reading to see a way around this.
<-to access values in a monad
We have the following code:
import qualified Data.Text as T main = do cliArgs <- getArgs let values = fmap (T.unpack . T.toTitle . T.pack) cliArgs putStrLn $ "Titled args: " ++ (show values)
getArgs gives you a list of the arguments passed on the command line and has the type
cliArgs <- getArgs we have access to the
[String] part, which is the list of arguments.
Here is another example with a different function:
main = do fileContents <- readFile "words.txt" let charCount = length fileContents putStrLn $ "File words.txt has " ++ (show charCount) ++ " chars."
readFile reads the contents of a file and has the type
FilePath -> IO String. So the function takes a
FilePath and returns
IO String. Again, by using
<- we get access to
String which represents the contents of the file.
Sometimes, we have the opposite situation though. Namely, we want to put a value in the monad. For example:
main = do putStrLn "Hello! What's your name?" putStr "Name: " name <- getLine putStrLn $ "Hello, " ++ name
putStrLn prints a string and has the type
String -> IO (). As you can see, it returns
IO (). In this case,
IO doesn't give us a meaningful value, just
(), so we don't use
<-. We just put the function call on its own line.
Here is another example:
import qualified Data.Text as T main = do fileContents <- readFile "words.txt" let fileWords = words fileContents titledWords = fmap (T.unpack . T.toTitle . T.pack) fileWords newFileContents = unwords titledWords writeFile "titled-words.txt" newFileContents
The relevant function here is
writeFile. As the name suggests, it writes a string to a file and it has the type
FilePath -> String -> IO ().
IO () again as the return type.
As a general rule, when we have
IO a for any meaningful value of
a, it makes sense to use
<-. When we have
IO (), it doesn't.
letfor pure code
In several examples above, I used
let. If it's not clear from the examples,
let is like an escape hatch. That means that it allows you to write:
All your Haskell programs are an example of the first case. You use
IO to get something from the command line or from a file. Then, you process that something and you use
IO again to write it to a file or to the terminal.
Btw, this is diferent from
let ... in ..., which is an expression that you use outside a
The most simple example would be:
main = do cliArgs <- getArgs let magicEffect = doMagic cliArgs putStrLn $ "Result of magic: " ++ magicEffect
doMagic has type
[String] -> String. As you can see, there's no
IO in the type.
Here's an example where
let allows us to nest a
do block that uses a different monad:
main = do cliArgs <- getArgs let result = do head_ <- headMaybe cliArgs int <- readMaybe head_ :: Maybe Int return $ show $ int ^ 2 putStrLn $ fromMaybe "N/A" result
This function gets the arguments passed on the command line.
Then, it extracts the first one and converts it to an
Int. Then, it does the totally useless thing of calculating its square, because I couldn't think of a more meaningful example. All this is happening in the
Finally, we print the result, having
"N/A" as a default, in case one of the functions inside the nested
do block returned
You can use more than one
let block and intermingle it with monadic code. For example:
main = do putStrLn "Hello, user! What's your name?" putStr "Enter name: " name <- getLine let boldedName = "**" ++ name ++ "**" putStrLn $ "Greetings, " ++ boldedName ++ "! Nice chatting with you." putStrLn "Why do you want to learn Haskell?" putStrLn "a) For fun" putStrLn "b) For profit" putStrLn "c) Both" putStr "Enter choice: " choice <- getLine let answer = case choice of "a" -> "Aha. Fun is your middle name." "b" -> "No time for games, eh?" "c" -> "Are you a Libra?" _ -> "So you don't like to play by the rules, ... or you were just confused?" putStrLn answer putStrLn "Good luck! Bye!"
doblock must yield a value of your monad
If you look at all the examples I've given so far, you'll notice that all of them end with a function that returns
IO. That's because the
do block must yield a value of our monad.
We have 2 options here:
We've seen examples of the first case, so let's look at an example for the second one:
titledHead :: [String] -> Maybe String titledHead items = do ourHead <- headMaybe items let titledHead = T.unpack . T.toTitle . T.pack $ ourHead return titledHead
Our function deals with the happy path. If
items has at least one item, it's extracted as
ourHead. We can apply the transformations we want and we get
titledHead has the type
String, but our function must return
Maybe String, so we use
return to wrap our
In case, it's not obvious, you can use other expressions inside a
do block, as long as they yield the right value:
main = do putStrLn "Do you like pizza?" putStrLn "a) Yes" putStr "Choice: " choice <- getLine if choice == "a" then putStrLn "Benvenuto!" else putStrLn "We don't want you here."
main = do putStrLn "Pick a number between 1 and 3" putStr "Number: " number <- getLine case number of "1" -> putStrLn "First!" "2" -> putStrLn "Second!" "3" -> putStrLn "What was left." _ -> putStrLn "Typo?"
To summarize, inside a
do block, you can:
return, so you create a monadic value
PS: Not all monads support all of these operations. For example, you can't put a value in the