Learn You a Haskell for Great Good! - Miran Lipovaca [71]
Let’s take a look at an I/O action that uses both <- and let to bind names.
import Data.Char
main = do
putStrLn "What's your first name?"
firstName <- getLine
putStrLn "What's your last name?"
lastName <- getLine
let bigFirstName = map toUpper firstName
bigLastName = map toUpper lastName
putStrLn $ "hey " ++ bigFirstName ++ " "
++ bigLastName
++ ", how are you?"
See how the I/O actions in the do block are lined up? Also notice how the let is lined up with the I/O actions, and the names of the let are lined up with each other? That’s good practice, because indentation is important in Haskell.
We wrote map toUpper firstName, which turns something like "John" into a much cooler string like "JOHN". We bound that uppercased string to a name and then used it in a string that we printed to the terminal.
You may be wondering when to use <- and when to use let bindings. <- is for performing I/O actions and binding their results to names. map toUpper firstName, however, isn’t an I/O action—it’s a pure expression in Haskell. So you can use <- when you want to bind the results of I/O actions to names, and you can use let bindings to bind pure expressions to names. Had we done something like let firstName = getLine, we would have just called the getLine I/O action a different name, and we would still need to run it through a <- to perform it and bind its result.
Putting It in Reverse
To get a better feel for doing I/O in Haskell, let’s make a simple program that continuously reads a line and prints out the same line with the words reversed. The program’s execution will stop when we input a blank line. This is the program:
main = do
line <- getLine
if null line
then return ()
else do
putStrLn $ reverseWords line
main
reverseWords :: String -> String
reverseWords = unwords . map reverse . words
To get a feel for what it does, save it as reverse.hs, and then compile and run it:
$ ghc --make reverse.hs
[1 of 1] Compiling Main ( reverse.hs, reverse.o )
Linking reverse ...
$ ./reverse
clean up on aisle number nine
naelc pu no elsia rebmun enin
the goat of error shines a light upon your life
eht taog fo rorre senihs a thgil nopu ruoy efil
it was all a dream
ti saw lla a maerd
Our reverseWords function is just a normal function. It takes a string like "hey there man" and applies words to it to produce a list of words like ["hey","there","man"]. We map reverse over the list, getting ["yeh","ereht","nam"], and then we put that back into one string by using unwords. The final result is "yeh ereht nam".
What about main? First, we get a line from the terminal by performing getLine and call that line line. Next we have a conditional expression. Remember that in Haskell, every if must have a corresponding else, because every expression must have some sort of value. Our if says that when a condition is true (in our case, the line that we entered is blank), we perform one I/O action; when it isn’t true, the I/O action under the else is performed.
Because we need to have exactly one I/O action after the else, we use a do block to glue together two I/O actions into one. We could also write that part as follows:
else (do
putStrLn $ reverseWords line
main)
This makes it clearer that the do block can be viewed as one I/O action, but it’s uglier.
Inside the do block, we apply reverseWords to the line that we got from getLine and then print that to the terminal. After that, we just perform main. It’s performed recursively, and that’s okay, because main is itself an I/O action. So in a sense, we go back to the start of the program.
If null line is True, the code after the then is executed: return (). You might have used a return keyword in other languages to return from a subroutine or function. But return in Haskell is nothing like the return in most other languages.
In Haskell (and in I/O actions specifically), return makes an I/O action out of a pure value.