Learn You a Haskell for Great Good! - Miran Lipovaca [72]
What’s the point of just transforming a pure value into an I/O action that doesn’t do anything? Well, we needed some I/O action to carry out in the case of an empty input line. That’s why we made a bogus I/O action that doesn’t do anything by writing return ().
Unlike in other languages, using return doesn’t cause the I/O do block to end in execution. For instance, this program will quite happily continue all the way to the last line:
main = do
return ()
return "HAHAHA"
line <- getLine
return "BLAH BLAH BLAH"
return 4
putStrLn line
Again, all these uses of return do is make I/O actions that yield a result, which is then thrown away because it isn’t bound to a name.
We can use return in combination with <- to bind stuff to names:
main = do
a <- return "hell"
b <- return "yeah!"
putStrLn $ a ++ " " ++ b
So you see, return is sort of the opposite of <-. While return takes a value and wraps it up in a box, <- takes a box (and performs it) and takes the value out of it, binding it to a name. But doing this is kind of redundant, especially since you can use let in do blocks to bind to names, like so:
main = do
let a = "hell"
b = "yeah"
putStrLn $ a ++ " " ++ b
When dealing with I/O do blocks, we mostly use return either because we need to create an I/O action that doesn’t do anything or because we don’t want the I/O action that’s made up from a do block to have the result value of its last action. When we want it to have a different result value, we use return to make an I/O action that always yields our desired result, and we put it at the end.
Some Useful I/O Functions
Haskell comes with a bunch of useful functions and I/O actions. Let’s take a look at some of them to see how they’re used.
putStr
putStr is much like putStrLn, in that it takes a string as a parameter and returns an I/O action that will print that string to the terminal. However, putStr doesn’t jump into a new line after printing out the string, whereas putStrLn does. For example, look at this code:
main = do
putStr "Hey, "
putStr "I'm "
putStrLn "Andy!"
If we compile and run this, we get the following output:
Hey, I'm Andy!
putChar
The putChar function takes a character and returns an I/O action that will print it to the terminal:
main = do
putChar 't'
putChar 'e'
putChar 'h'
putStr can be defined recursively with the help of putChar. The base case of putStr is the empty string, so if we’re printing an empty string, we just return an I/O action that does nothing by using return (). If it’s not empty, then we print the first character of the string by doing putChar and then print the rest of them recursively:
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do
putChar x
putStr xs
Notice how we can use recursion in I/O, just as we can use it in pure code. We define the base case and then think what the result actually is. In this case, it’s an action that first outputs the first character and then outputs the rest of the string.
print takes a value of any type that’s an instance of Show (meaning that we know how to represent it as a string), applies show to that value to “stringify” it, and then outputs that string to the terminal. Basically, it’s just putStrLn . show. It first runs show on a value, and then feeds that to putStrLn, which returns an I/O action that will print out our value.
main = do
print True
print 2
print "haha"
print 3.2
print [3,4,3]
Compiling this and running it, we get the following output:
True
2
"haha"
3.2
[3,4,3]
As you can see, it’s a very handy function. Remember how we talked about how I/O actions are performed only when they fall into main or when we try to evaluate them at the GHCi prompt? When we type out a value (like 3 or [1,2,3]) and press enter, GHCi actually uses print on that