6 Aprilie 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 getArgs
, readFile
, writeFile
do
notationIn 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 do
block.
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 monadWe 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 IO [String]
.
By doing 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.
let
for pure codeIn 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 do
block.
The most simple example would be:
main = do
cliArgs <- getArgs
let
magicEffect = doMagic cliArgs
putStrLn $ "Result of magic: " ++ magicEffect
where 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 Maybe
monad.
Finally, we print the result, having "N/A"
as a default, in case one of the functions inside the nested do
block returned Nothing
.
let
sYou 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!"
do
block must yield a value of your monadIf 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:
return
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
. titledHead
has the type String
, but our function must return Maybe String
, so we use return
to wrap our String
.
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."
Or:
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 valuePS: Not all monads support all of these operations. For example, you can't put a value in the Reader
monad.